20200516のiOSに関する記事は9件です。

UIButtonをimageをセットしたが表示されない件

UIButtonにsetImage()したけど画像の色が違う

UIButtonを画像にしたい+.normal.selectedで画像を切り替えたいと思った時に躓きました。

● 実装(Rx使ってます)
private func setButton() {
    button.setImage(R.image.grayCat(), for: .normal)
    button.setImage(R.image.purpleCat(), for: .selected)
    button.rx.tap.bind(to: buttonTapBinder).disposed(by: disposeBag)
}

private var buttonTapBinder: Binder<()> {
    return Binder(self) { base, _  in
    base.button.isSelected = !base.button.isSelected
    }
}
● 使った画像

実行画面

ボタンタップしたレイアウトもちがーーーーーーーーーーーう!!!!

解決策

私は普段.xibファイルで部品の配置とAutlayoutの設定をしているのですが、その際のButtonTypeが原因でした。
スクリーンショット 2020-05-16 21.23.23.png
これをCustomにすれば解決です!

このButtonTypeget-onlyなのでコードからは変更できません。
ちなみにレイアウト系もコードで実装している場合は、初期値がCustomなので特に問題はないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UIButtonをimageをセットしたが表示されない件

UIButtonにsetImage()したけど画像の色が違う

UIButtonを画像にしたい+.normal.selectedで画像を切り替えたいと思った時に躓きました。

● 実装(Rx使ってます)
private func setButton() {
    button.setImage(R.image.grayCat(), for: .normal)
    button.setImage(R.image.purpleCat(), for: .selected)
    button.rx.tap.bind(to: buttonTapBinder).disposed(by: disposeBag)
}

private var buttonTapBinder: Binder<()> {
    return Binder(self) { base, _  in
    base.button.isSelected = !base.button.isSelected
    }
}
● 使った画像

実行画面

ボタンタップしたレイアウトもちがーーーーーーーーーーーう!!!!

解決策

私は普段.xibファイルで部品の配置とAutlayoutの設定をしているのですが、その際のButtonTypeが原因でした。
スクリーンショット 2020-05-16 21.23.23.png
これをCustomにすれば解決です!

このButtonTypeget-onlyなのでコードからは変更できません。
ちなみにレイアウト系もコードで実装している場合は、初期値がCustomなので特に問題はないです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSリバーシリファクタリングチャレンジ w/ Redux

リバーシリファクタリングチャレンジ

koherさんが公開された、このFat View Controller、あなたはリファクタリングできますか?チャレンジに参加しました。

本チャレンジは、 Fat View Controller として実装されたリバーシアプリをリファクタリングし、どれだけクリーンな設計とコードを実現できるかというコンペティションです(ジャッジが優劣を判定するわけではなく、設計の技を競い合うのが目的です)。

すばらしいチャレンジを用意くださったkoherさんを始め、運営のお手伝いをされているtakasekさん、Ogawaさんの皆様に感謝です。

リファクタリング結果

以下のGitHubリポジトリにリファクタリングした結果を公開しています。masterブランチがリファクタリング済みになります。
この記事にもコードを記載していますが全体の8割ぐらいです。

リファクタリング方針

ゼロから作り直すやり方ではなく、オリジナルのリバーシ部分のロジックを踏襲しつつ、少しずつ動作を確認しながらリファクタリングを実施しました。

iOSアプリ設計パターン入門にて、Reduxアーキテクチャの章を執筆したので、Reduxアーキテクチャを適用したリファクタリングにチャレンジします。ターン制のゲームはユーザーやコンピュータのアクションによりゲームの状態が変化し、状態の変化に合わせたビューの表示を行う流れは、Reduxアーキテクチャと相性が良いと感じました。

リファクタリング観点

以下のような観点を意識しながら、リファクタリングを行いました。
ただ、各観点を最大化することが目的ではなく、適切な範囲にとどめ全体最適を目指すことが大事だと思っています。

  • 階層、分断、排他、網羅を意識した構造化
  • 影響範囲の局所化
  • 型表現や命名による意味付け
  • 単一責務化
  • 制約化
  • 抽象化
  • 共通化

アーキテクチャ導入

Reduxアーキテクチャ構造

リバーシアプリに適用したReduxアーキテクチャの構造は以下の図のようになります。
また、Reduxアーキテクチャを支援するReSwiftライブラリを導入しました。

ReduxArchitecture.001.jpeg

Reduxは状態の変化を単一のフロー制御により制約付けられており、ViewからはActionをStoreにdispatchし、Reducer関数によってStateが変更されます。ViewはStoreから変更されたことが通知されるので、イミュータブルなStateにアクセスし前回のStateから変化した状態を割り出して適切なViewの表示を更新したり、Stateの状態を鑑みて新たなActionをStoreにdispatchしたりします。

レイヤ分割(Xcodeターゲット分割)

Reduxアーキテクチャを導入するにあたりターゲットを以下の2つに分割しました。
もっと規模が大きいアプリの場合は、LogicレイヤをCleanアーキテクチャを参考にレイヤ分割してもよいかもしれません。

  • Reversiターゲット(ビューに関するコード)
    • UIKit ViewController
    • UIKit View
  • Logicターゲット(ロジックに関するコード)
    • Redux actions
    • Redux store
    • Redux state
    • Redux reducer

依存関係は以下のとおりです。

  • Reversiターゲット ⇒ Logicターゲット

Logicターゲットは以下のような目的で設けました。

  • Viewのコードに依存しないビジネスロジックのコードを局所化するため
  • UIKitに依存しないPureSwiftのコードを局所化するため
  • ビジネスロジックのコードのViewに対する可視性を細かく制御(publicとinternalの使い分け)したかったため

可視性制御(publicとinternalの使い分け)

  • stateやdataのstruct/classはロジック側でのみinitできるように制約する
    • ⇒ View側で意図せず状態が生成され利用されることを抑止する
  • internalなプロパティはテストコードからでも参照できるので、より詳細な内部の状態の確認に利用する

DataTypeの抽出

  • 責務過多なデータ表現は用途を限定した単一責務のデータに分割する
    • e.g. Disk and Side
  • 役割を担うプリミティブなデータ構造をデータ型と命名を与え意味付する
    • e.g. Coordinate, PlacedDiskCoordinate, OptionalDiskCoordinate
  • 有限なデータを可変長[配列]で表現している箇所は有限で表現する
  • ビューのインスタンスが保持しているデータをDataTypeとして抽出する

既存のDataType

public enum Player: Int, Equatable, Codable {
    case manual = 0
    case computer = 1
}

public enum Disk: String, Codable {
    case diskDark
    case diskLight
}

追加したDataType

public enum Side: String, Codable, CaseIterable {
    case sideDark
    case sideLight
}

extension Side: Hashable {}

extension Side {
    public var index: Int {
        switch self {
        case .sideDark: return 0
        case .sideLight: return 1
        }
    }

    public var disk: Disk {
        switch self {
        case .sideDark: return .diskDark
        case .sideLight: return .diskLight
        }
    }
}

extension Side {
    var flipped: Side {
        switch self {
        case .sideDark: return .sideLight
        case .sideLight: return .sideDark
        }
    }
}

public struct Coordinate: Equatable, Codable {
    public var x: Int
    public var y: Int

    public init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

infix operator +: AdditionPrecedence
extension Coordinate {
    static func + (left: Coordinate, right: Coordinate) -> Coordinate {
        return Coordinate(x: left.x + right.x, y: left.y + right.y)
    }
}

public struct PlacedDiskCoordinate: Equatable, Codable {
    public var disk: Disk
    public var coordinate: Coordinate

    public init(disk: Disk, coordinate: Coordinate) {
        self.disk = disk
        self.coordinate = coordinate
    }
}

extension PlacedDiskCoordinate {
    var optionalDiskCoordinate: OptionalDiskCoordinate { OptionalDiskCoordinate(disk: disk, coordinate: coordinate) }
}

public struct OptionalDiskCoordinate: Equatable, Codable {
    public var disk: Disk?
    public var coordinate: Coordinate
}

public struct BoardSetting: Equatable, Codable {
    public var cols: Int
    public var rows: Int

    private var xRange: Range<Int> { 0 ..< self.cols }
    private var yRange: Range<Int> { 0 ..< self.rows }

    public var coordinates: [Coordinate] {
        self.yRange.map { y in self.xRange.map { x in Coordinate(x: x, y: y) } }.flatMap { $0 }
    }

    public func validCoordinate(_ coordinate: Coordinate) -> Bool {
        self.xRange.contains(coordinate.x) && self.yRange.contains(coordinate.y)
    }

    init(cols: Int, rows: Int) {
        self.cols = cols
        self.rows = rows
    }
}

補足

データがEquatableに準拠しているのはView側でStateの変更通知を受け取り、Stateへアクセスしたときに前回の変更通知から、どこが変わったのかわからないため前回のStateと比較して差分を検知するために利用します。

GameProgress(ゲーム進行状態表現)

ゲームの進行状態を表現するGameProgressを導入しました。

  • このゲームの進行状態は有限でかつ排他的なのでenumで表現
  • 各進行状態下に付随する取りうる状態をassociated valueを活用して包括関係を表現
public enum GameProgress: Equatable {
    case initialing
    case turn(Progress, Side, Player, ComputerThinking)
    case gameOver(GameOver)
    case interrupt(Interrupt)
}

public enum Side: String, Codable, CaseIterable {
    case sideDark
    case sideLight
}

public enum ComputerThinking: String, Equatable, Codable {
    case none
    case thinking
}

public enum GameOver: Equatable {
    case won(Side)
    case tied
}

public enum Interrupt: Equatable {
    case resetConfrmation(Alert)
    case cannotPlaceDisk(Alert)
}

public enum Alert: String, Equatable, Codable {
    case none
    case shouldShow
    case showing
}

public enum Progress: Equatable {
    case start
    case progressing
}

以下の表はGameProgressが取りうる主要な状態のパターンを表しています。
(注意:この表は便宜上のため、正しい包括関係と網羅性を担保できていません)

GameProgress Associated value1 Associated value2 Associated value3
initialing --- --- ---
turn Side.dark Player.manual ComputerThinking.none
Player.computer ComputerThinking.none
ComputerThinking.thinking
Side.light Player.manual ComputerThinking.none
Player.computer ComputerThinking.none
ComputerThinking.thinking
gameOver GameOver.won Side.dark ---
Side.light ---
GameOver.tied --- ---
interrupt resetConfirmation Alert.none ---
Alert.shouldShow ---
Alert.showing ---
cannotPlaceDisk Alert.none ---
Alert.shouldShow ---
Alert.showing ---

interrup(割り込み状態表現)

アラートを表示中の状態管理が悩ましかったのでinterrupt状態を導入しゲーム中への割り込みを表現しています。
既存コードでは、リセットのアラートを表示中でも双方のプレイヤーがコンピュータであればゲームは進行します。
このとき、プレイヤーが手詰まりになると、手詰まりのアラートを表示するのですが、2つのアラート表示がバッティングするので対処が必要になります。

本リファクタリングでは、リセットアラートを表示中はコンピュータによるゲームを進行しない方針としました。
interrupt状態はturn状態と排他的な状態で、ゲームの進行(コンピュータの思考)はturn状態のみ実行できるものとすることで実現しました。

また、interruptは手詰まりのアラートをシステムから表示する場合も割り込みとして表現しました。

既存コード リファクタリングコード
リセットアラートが表示中でもゲームが進行する リセットアラートが表示中だとゲームが進行しない
May-16-2020 02-26-45.gif May-16-2020 02-16-11.gif

Redux

AppStateとAcrtionのすべてのコードはGitHub repositoryで確認できます。

AppState

AppStateはViewから参照される状態の起点になるStateです。

GameProgressAppStateの内部状態から現在あるべきGameProgressの状態をComputed propertyを用いて割り出しています。

import Foundation
import ReSwift

public struct AppState: StateType, Codable {
    public var boardContainer: BoardContainer
    public var playerDark: PlayerSide = .init(side: .sideDark)
    public var playerLight: PlayerSide = .init(side: .sideLight)
    public var gameProgress: GameProgress {
        if isInitialing {
            return .initialing
        } else if cannotPlaceDiskAlert != .none {
            return .interrupt(.cannotPlaceDisk(cannotPlaceDiskAlert))
        } else if resetConfirmationAlert != .none {
            return .interrupt(.resetConfirmation(resetConfirmationAlert))
        } else if let side = side {
            let progress: Progress = turnStart ? .start : .progressing
            let player: Player
            switch side {
            case .sideDark: player = playerDark.player
            case .sideLight: player = playerLight.player
            }
            return .turn(progress, side, player, computerThinking)
        } else if let winnerSide = boardContainer.board.sideWithMoreDisks() {
            return .gameOver(.won(winnerSide))
        } else {
            return .gameOver(.tied)
        }
    }

    var id: String = NSUUID().uuidString // prevent override uing reseted state
    var side: Side? = .sideDark
    var turnStart: Bool = false
    var isInitialing: Bool = true
    var isLoadedGame: Bool = false // prevent duplicate load game calls
    var computerThinking: ComputerThinking = .none
    var cannotPlaceDiskAlert: Alert = .none
    var resetConfirmationAlert: Alert = .none

    init(boardSetting: BoardSetting = .init(cols: 8, rows: 8)) {
        self.boardContainer = .init(diskCoordinatesState: Board(boardSetting: boardSetting))
    }
}

Reducer

Reducerはミューテーション可能なStateのコピーを一時的に生成し、新たな状態を反映させたうえ、イミュータブルなStateとして返す純粋関数です。
各Actionによって、どのように状態が変化するのか一目瞭然となっています。

func reducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? .init()

    if state.turnStart {
        state.turnStart = false
    }

    if let action = action as? AppAction {
        switch action {
        case .startGame:
            state.isInitialing = false
        case .placeDisk(let placedDiskCoordinate):
            let flippedDiskCoordinates = state.boardContainer.board.flippedDiskCoordinatesByPlacingDisk(placedDiskCoordinate)
            guard !flippedDiskCoordinates.isEmpty else { return state }

            let changed: BoardChanged = .init(placedDiskCoordinate: placedDiskCoordinate, flippedDiskCoordinates: flippedDiskCoordinates)
            changed.changedDiskCoordinate.forEach { state.boardContainer.board[$0.coordinate] = $0.optionalDiskCoordinate }
            state.boardContainer.changed = changed
            state.playerDark.count = state.boardContainer.board.count(of: .diskDark)
            state.playerLight.count = state.boardContainer.board.count(of: .diskLight)
        case .cannotPlaceDisk(let alert):
            state.cannotPlaceDiskAlert = alert
        case .resetConfirmation(let alert):
            state.resetConfirmationAlert = alert
        }
    }
    if let action = action as? AppPrivateAction {
        switch action {
        case .nextTurn:
            guard case .none = state.resetConfirmationAlert else { return state }
            guard let temp = state.side else { assertionFailure(); return state }
            state.cannotPlaceDiskAlert = .none
            let side = temp.flipped
            state.side = side
        case .validateTurn:
            guard let side = state.side else { return state }
            if state.boardContainer.board.validMoves(for: side).isEmpty {
                if state.boardContainer.board.validMoves(for: side.flipped).isEmpty {
                    state.side = nil // GameOver
                } else {
                    state.cannotPlaceDiskAlert = .shouldShow
                }
            } else {
                state.turnStart = true
            }
        case .changePlayer(let side, let player):
            switch side {
            case .sideDark: state.playerDark.player = player
            case .sideLight: state.playerLight.player = player
            }
            state.turnStart = true
            if side == state.side {
                state.computerThinking = .none
            }
        case .resetAllState:
            var newState = AppState()
            newState.playerDark = .init(side: .sideDark, count: newState.boardContainer.board.count(of: .diskDark))
            newState.playerLight = .init(side: .sideLight, count: newState.boardContainer.board.count(of: .diskLight))
            return newState
        case .finisedLoadGame(let loadedAppState):
            return loadedAppState
        case .finisedSaveGame:
            break
        case .startComputerThinking:
            state.computerThinking = .thinking
        case .endComputerThinking:
            state.computerThinking = .none
        }
    }
    return state
}

Action

Actionはenumで表現して、Reducerで処理を行うべき網羅性を担保しています。
PrivateActionは、Viewからdispatchしない、ActionCreatorからのみdisptachするActionとして設けています。

public enum AppAction: Action {
    case startGame
    case placeDisk(PlacedDiskCoordinate)
    case cannotPlaceDisk(Alert)
    case resetConfirmation(Alert)
}

enum AppPrivateAction: Action {
    case nextTurn
    case validateTurn
    case changePlayer(side: Side, player: Player)
    case resetAllState
    case finisedLoadGame(AppState)
    case startComputerThinking
    case endComputerThinking
    case finisedSaveGame
}

struct ErrorAction: Action {
    let error: Error
    let title: String
    let message: String
}

ActionCreator

ActionCreatorReducerのようにStateの変更を行いませんが、以下のようなロジックの処理を担っています。

  • 副作用(ゲームデータの保存・読み込み)が発生する処理
  • 条件によってdispatchしたいActionを変更したい処理
  • 複数のActionをdispatchしたい処理

また、副作用を伴う処理に依存する部分は、テスタブルにするためDependency Injectionしています。

extension AppAction {
    public static func newGame() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.newGame() START")
            dispatch(AppPrivateAction.resetAllState)
            dispatch(AppAction.saveGame())
            print("- Logic.AppAction.newGame() END")
        }
    }

    public static func saveGame() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.saveGame() START")
            do {
                guard var state = getState() else { preconditionFailure() }
                state.isInitialing = true
                state.boardContainer.changed = nil
                state.computerThinking = .none
                state.resetConfirmationAlert = .none
                try dependency.persistentInteractor.saveGame(state)
                dispatch(AppPrivateAction.finisedSaveGame)
            } catch let error {
                dump(error)
                let title = "Error occurred."
                let message = "Cannot save games."
                dispatch(ErrorAction(error: error, title: title, message: message))
            }
            print("- Logic.AppAction.saveGame() END")
        }
    }

    public static func loadGame() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.loadGame() START")
            do {
                guard let state = getState() else { preconditionFailure() }
                guard state.isLoadedGame == false else { return } // prevent duplicate load game calls
                dispatch(AppPrivateAction.resetAllState)
                let loadData = try dependency.persistentInteractor.loadGame()
                dispatch(AppPrivateAction.finisedLoadGame(loadData))
                dispatch(AppPrivateAction.validateTurn)
            } catch let error {
                dump(error)
                dispatch(AppAction.newGame())
            }
            print("- Logic.AppAction.loadGame() END")
        }
    }

    public static func nextTurn() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            guard let state = getState() else { preconditionFailure() }
            if case .turn(_, let side, _, _) = state.gameProgress {
                print("- Logic.AppAction.nextTurn() from: \(side) to: \(side.flipped)")
            }
            dispatch(AppPrivateAction.nextTurn)
            dispatch(AppPrivateAction.validateTurn)
        }
    }

    public static func changePlayer(side: Side, player: Player) -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.changePlayer(side: \(side), player: \(player)) START")
            dispatch(AppPrivateAction.changePlayer(side: side, player: player))
            dispatch(AppAction.saveGame())
            print("- Logic.AppAction.changePlayer(side: \(side), player: \(player)) END")
        }
    }

    public static func waitForPlayer() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.waitForPlayer() START")
            guard let state = getState() else { preconditionFailure() }
            switch state.gameProgress {
            case .turn(_, _, let player, _):
                switch player {
                case .manual:
                    break
                case .computer:
                    dispatch(AppAction.playTurnOfComputer())
                }
            case .initialing, .interrupt, .gameOver:
                assertionFailure()
            }
            print("- Logic.AppAction.waitForPlayer() END")
        }
    }

    private static func playTurnOfComputer() -> Thunk<AppState> {
        return Thunk<AppState> { dispatch, getState, dependency in
            print("- Logic.AppAction.playTurnOfComputer() START")
            guard let state = getState() else { preconditionFailure() }
            switch state.gameProgress {
            case .turn(_, let side, _, _):
                let candidates = state.boardContainer.board.validMoves(for: side)
                switch candidates.isEmpty {
                case true:
                    dispatch(AppAction.nextTurn())
                case false:
                    guard let candidate = candidates.randomElement() else { preconditionFailure() }
                    let id = state.id
                    store.dispatch(AppPrivateAction.startComputerThinking)
                    DispatchQueue.main.asyncAfter(deadline: .now() + dependency.computerThinkingTime) {
                        guard let state = getState() else { preconditionFailure() }
                        guard id == state.id else { return } // maybe reset game
                        guard case .turn(_, let sideEnd, _, let computerThinkingEnd) = state.gameProgress else { return }
                        guard case .thinking = computerThinkingEnd, side == sideEnd else { return } // maybe chaned to manual player
                        guard case .none = state.resetConfirmationAlert else { return }
                        dispatch(AppPrivateAction.endComputerThinking)
                        dispatch(AppAction.placeDisk(candidate))
                    }
                }
            case .initialing, .interrupt, .gameOver:
                assertionFailure()
            }
            print("- Logic.AppAction.playTurnOfComputer() END")
        }
    }
}

Dependency & Middleware

PersistentInteractor,RepositoryはDependency Injectionできようにすために、protocolで抽象化し、createThunkMiddleware経由でDIしています。

public let store = Store<AppState>(
    reducer: reducer,
    state: AppState(),
    middleware: [thunkMiddleware, loggingMiddleware]
)

protocol Dependency {
    var persistentInteractor: PersistentInteractor { get }
    var computerThinkingTime: DispatchTimeInterval { get }
}

struct DependencyImpl: Dependency {
    let persistentInteractor: PersistentInteractor
    let computerThinkingTime: DispatchTimeInterval

    init(persistentInteractor: PersistentInteractor = PersistentInteractorImpl(), computerThinkingTime: DispatchTimeInterval = DispatchTimeInterval.milliseconds(1000)) {
        self.persistentInteractor = persistentInteractor
        self.computerThinkingTime = computerThinkingTime
    }
}

let thunkMiddleware: Middleware<AppState> = createThunkMiddleware()

public struct Thunk<State>: Action {
    let body: (_ dispatch: @escaping DispatchFunction, _ getState: @escaping () -> State?, _ dependency: Dependency) -> Void
    init(body: @escaping (
        _ dispatch: @escaping DispatchFunction,
        _ getState: @escaping () -> State?,
        _ dependency: Dependency) -> Void) {
        self.body = body
    }
}

func createThunkMiddleware<State>(dependency: Dependency = DependencyImpl()) -> Middleware<State> {
    return { dispatch, getState in
        return { next in
            return { action in
                switch action {
                case let thunk as Thunk<State>:
                    thunk.body(dispatch, getState, dependency)
                default:
                    next(action)
                }
            }
        }
    }
}

let loggingMiddleware: Middleware<AppState> = { dispatch, getState in
    return { next in
        return { action in
            dump(action)
            if case AppPrivateAction.validateTurn = action {
                print(getState()?.boardContainer.board.debugDescription ?? "N/A")
            }
            return next(action)
        }
    }
}

PersistentInteractor & Repository

ゲーム状態をファイルに保存・読み込みするためのロジックです。
既存では独自のファイルファーマット形式で保存していましたが、ReduxのStateをCodableに準拠してJSON形式でStateを丸ごと保存するようにしました。

メリット

  • 独自のファイルファーマット形式のパースロジックを排除し、一般的でかつパースAPIが提供されているJSONを利用できた

デメリット

  • Stateの構造を変更すると、下位互換がなくなってしまいデータをロードできなくなってしまった

結論

  • 過剰なリファクタリングでした
protocol PersistentInteractor {
    func saveGame(_ appState: AppState) throws /* PersistentError */
    func loadGame() throws -> AppState /*  PersistentError */
}

struct PersistentInteractorImpl: PersistentInteractor {
    enum PersistentError: Error {
        case write(cause: Error?)
        case read(cause: Error?)
    }

    private let repository: Repository

    init(repository: Repository = RepositoryImpl()) {
        self.repository = repository
    }

    func encode(_ appState: AppState) throws -> Data {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        return try encoder.encode(appState)
    }

    func saveGame(_ appState: AppState) throws {
        do {
            let data = try encode(appState)
            try repository.saveData(data)
        } catch let error {
            throw PersistentError.read(cause: error)
        }
    }

    func loadGame() throws -> AppState {
        do {
            let data = try repository.loadData()
            return try JSONDecoder().decode(AppState.self, from: data)
        } catch let error {
            throw PersistentError.write(cause: error)
        }
    }
}

extension Coordinate { /* Codable */
    enum CodingKeys: String, CodingKey {
        case x
        case y
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
    }
}

ファイルに保存するときのファイル名はコンストラクタインジェクションできるようにし、テスト時は変更できるようにしました。
アプリ実行時とテスト実行時で保存したファイルを競合しないようにするためです。

protocol Repository {
    func saveData(_ data: Data) throws /* FileIOError */
    func loadData() throws -> Data /* FileIOError */
    func clear() throws /* FileIOError */
}

struct RepositoryImpl: Repository {
    enum FileIOError: Error {
        case write(cause: Error?)
        case read(cause: Error?)
        case clear(cause: Error?)
    }

    let fileName: String

    init(fileName: String = "appstate.json") {
        self.fileName = fileName
    }

    private func createFileURL() throws -> URL {
        try FileManager.default
            .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent(fileName)
    }

    func saveData(_ data: Data) throws {
        do {
            let fileURL = try createFileURL()
            try data.write(to: fileURL, options: [])
        } catch let error {
            throw FileIOError.read(cause: error)
        }
    }

    func loadData() throws -> Data {
        do {
            let fileURL = try createFileURL()
            return try Data(contentsOf: fileURL)
        } catch let error {
            throw FileIOError.write(cause: error)
        }
    }

    func clear() throws {
        do {
            let fileURL = try createFileURL()
            try FileManager.default.removeItem(at: fileURL)
        } catch let error {
            throw FileIOError.clear(cause: error)
        }
    }
}

ViewController

ViewControllerのすべてのコードはGitHub repositoryで確認できます。

ViewControllerのイニシャライザを利用してReduxのStreをコンストラクタインジェクションしたかっったので、storyboardからxibに変更しました。

class ViewController: UIViewController, StoreSubscriber {
    ...
    private let store: Store<AppState>

    init(store: Store<AppState> = Logic.store) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

ViewControllerのコードは3種類の役割に分割できます。

  • #1 / State handling(State -> Game management or View update)
  • #2 / Game management(Game management -> Dispatch Action)
  • #3 / View update

#1 / State handling(State -> Game management or View update)

Reduxから変更があった場合に通知を受、新たなStateの状態に基づいて、Game managementを行ったたり、Viewの表示を更新する指示を出しています。

  • func newState(state: AppState)はどんな変更であろうと通知され、表示を変更する処理を記載
  • subscriberGameProgressGameProgressに変更があった場合のみ通知され、GameProgressの状況をパターンマッチのうえ、各状況における適切な処理を記載
  • subscriberBoardContainerBoardContainerに変更があった場合のみ通知され、盤面の変更があった場合に盤面の表示を変更する処理を記載
class ViewController: UIViewController, StoreSubscriber {
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        boardView.delegate = self
        boardView.setUp(boardSetting: store.state.boardContainer.boardSetting)
        messageDiskSize = messageDiskSizeConstraint.constant
        store.subscribe(self)
        store.subscribe(subscriberGameProgress) { appState in appState.select { $0.gameProgress }.skipRepeats() }
        store.subscribe(subscriberBoardContainer) { appState in appState.select { $0.boardContainer }.skipRepeats() }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        loadGame()
    }

    func newState(state: AppState) {
        updatePlayerControls(state.gameProgress, playerSide: state.playerDark)
        updatePlayerControls(state.gameProgress, playerSide: state.playerLight)
        updateCountLabels(state.playerDark)
        updateCountLabels(state.playerLight)
        updateMessageViews(state.gameProgress)
    }

    private lazy var subscriberGameProgress = BlockSubscriber<GameProgress>() { [unowned self] in
        switch $0 {
        case .initialing:
            self.animationState.cancelAll()
            self.startGame()
        case .turn(let progress, let side, _, let computerThinking):
            self.updatePlayerActivityIndicators(side: side, computerThinking: computerThinking)
            switch progress {
            case .start:
                self.waitForPlayer()
            case .progressing:
                break
            }
        case .gameOver:
            break
        case .interrupt(let interrupt):
            switch interrupt {
            case .cannotPlaceDisk(let alert):
                switch alert {
                case .shouldShow:
                    self.showCannotPlaceDiskAlert()
                case .none, .showing:
                    break
                }
            case .resetConfirmation(let alert):
                switch alert {
                case .shouldShow:
                    self.showRestConfirmationAlert()
                case .none, .showing:
                    break
                }
            }
        }
    }
    private lazy var subscriberBoardContainer = BlockSubscriber<BoardContainer>() { [unowned self] in
        switch $0.changed {
        case .none:
            self.updateDisksForInitial($0.diskCoordinates)
        case .some(let changed):
            self.updateDisks(changed: changed, animated: true) { [weak self] _ in
                self?.nextTurn()
            }
        }
    }
}

Additional for ReSwift's subscriber

class BlockSubscriber<S>: StoreSubscriber {
    typealias StoreSubscriberStateType = S
    private let block: (S) -> Void

    init(_ block: @escaping (S) -> Void) {
        self.block = block
    }

    func newState(state: S) {
        self.block(state)
    }
}

#2 / Game management(Views -> State)

Game managementに関するメソッドが並びます。
View側からReduxのStateを変更するためのActionをdispatchする処理になります。

extension ViewController {
    func saveGame() {
        store.dispatch(AppAction.saveGame())
    }

    func loadGame() {
        store.dispatch(AppAction.loadGame())
    }

    func newGame() {
        animationState.cancelAll()
        store.dispatch(AppAction.newGame())
    }

    func startGame() {
        store.dispatch(AppAction.startGame)
    }

    func nextTurn() {
        store.dispatch(AppAction.nextTurn())
    }

    func waitForPlayer() {
        store.dispatch(AppAction.waitForPlayer())
    }

    func placeDisk(_ placedDiskCoordinate: PlacedDiskCoordinate) {
        store.dispatch(AppAction.placeDisk(placedDiskCoordinate))
    }

    func changePlayer(side: Side, player: Player) {
        store.dispatch(AppAction.changePlayer(side: side, player: player))
        animationState.cancel(at: side)
    }

    func cannotPlaceDisk(alert: Alert) {
        store.dispatch(AppAction.cannotPlaceDisk(alert))
    }

    func resetConfirmation(alert: Alert) {
        store.dispatch(AppAction.resetConfirmation(alert))
    }
}

#3 / View update(State -> Views)

Viewの表示を更新するためのメソッドが並びます。

extension ViewController {
   func updateMessageViews(_ gameProgress: GameProgress) {
      switch gameProgress {
      case .initialing, .interrupt:
          break
      case .turn(_, let side, _, _):
          messageDiskSizeConstraint.constant = messageDiskSize
          messageDiskView.disk = side.disk
          messageLabel.text = "'s turn"
      case .gameOver(let gameOver):
          switch gameOver {
          case .won(let winner):
              messageDiskSizeConstraint.constant = messageDiskSize
              messageDiskView.disk = winner.disk
              messageLabel.text = " won"
          case .tied:
              messageDiskSizeConstraint.constant = 0
              messageLabel.text = "Tied"
          }
      }
  }

  func updateDisksForInitial(_ diskCoordinates: [OptionalDiskCoordinate]) {
      diskCoordinates.forEach {
          boardView.updateDisk($0.disk, coordinate: $0.coordinate, animated: false)
      }
  }

  func updateDisks(changed: BoardChanged, animated isAnimated: Bool, completion: ((Bool) -> Void)? = nil) {
      let disk = changed.placedDiskCoordinate.disk
      let placedCoordinate = changed.placedDiskCoordinate.coordinate
      let flippedCoordinates = changed.flippedDiskCoordinates.map { $0.coordinate }

      if isAnimated {
          animationState.createAnimationCanceller()
          updateDisksWithAnimation(at: [placedCoordinate] + flippedCoordinates, to: disk) { [weak self] finished in
              guard let self = self else { return }
              if self.animationState.isCancelled { return }
              self.animationState.cancel()

              completion?(finished)
              self.saveGame()
          }
      } else {
          DispatchQueue.main.async { [weak self] in
              guard let self = self else { return }
              self.boardView.updateDisk(disk, coordinate: placedCoordinate, animated: false)
              flippedCoordinates.forEach {
                  self.boardView.updateDisk(disk, coordinate: $0, animated: false)
              }
              completion?(true)
              self.saveGame()
          }
      }
  }

  private func updateDisksWithAnimation<C: Collection>(at coordinates: C, to disk: Disk, completion: @escaping (Bool) -> Void)
      where C.Element == Coordinate
  {
      guard let coordinate = coordinates.first else {
          completion(true)
          return
      }

      boardView.updateDisk(disk, coordinate: coordinate, animated: true) { [weak self] finished in
          guard let self = self else { return }
          if self.animationState.isCancelled { return }
          if finished {
              self.updateDisksWithAnimation(at: coordinates.dropFirst(), to: disk, completion: completion)
          } else {
              coordinates.forEach {
                  self.boardView.updateDisk(disk, coordinate: $0, animated: false)
              }
              completion(false)
          }
      }
  }

  private func updatePlayerActivityIndicators(side: Side, computerThinking: ComputerThinking) {
      switch computerThinking {
      case .thinking:
          self.playerActivityIndicators[side.index].startAnimating()
      case .none:
          self.playerActivityIndicators.forEach { $0.stopAnimating() }
      }
  }

  func updatePlayerControls(_ gameProgress: GameProgress, playerSide: PlayerSide) {
      playerControls[playerSide.side.index].selectedSegmentIndex = playerSide.player.rawValue
      playerControls.forEach {
          switch gameProgress {
          case .turn:
              $0.isEnabled = true
          case .initialing, .interrupt, .gameOver:
              $0.isEnabled = false
          }
      }
  }

  func updateCountLabels(_ playerSide: PlayerSide) {
      countLabels[playerSide.side.index].text = "\(playerSide.count)"
  }

  func showCannotPlaceDiskAlert() {
      cannotPlaceDisk(alert: .showing)
      let alertController = UIAlertController(
          title: "Pass",
          message: "Cannot place a disk.",
          preferredStyle: .alert
      )
      alertController.addAction(UIAlertAction(title: "Dismiss", style: .default) { [weak self] _ in
          self?.cannotPlaceDisk(alert: .none)
          self?.nextTurn()
      })
      present(alertController, animated: true)
  }

  func showRestConfirmationAlert() {
      resetConfirmation(alert: .showing)
      let alertController = UIAlertController(
          title: "Confirmation",
          message: "Do you really want to reset the game?",
          preferredStyle: .alert
      )
      alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
          self?.resetConfirmation(alert: .none)
          self?.waitForPlayer()
      })
      alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
          self?.newGame()
      })
      present(alertController, animated: true)
  }
}

User inputs

extension ViewController {
    @IBAction func pressResetButton(_ sender: UIButton) {
        resetConfirmation(alert: .shouldShow)
    }

    @IBAction func changePlayerControlSegment(_ sender: UISegmentedControl) {
        guard let index = playerControls.firstIndex(of: sender) else { return }
        let side: Side
        switch index {
        case 0: side = .sideDark
        case 1: side = .sideLight
        default: preconditionFailure()
        }
        changePlayer(side: side, player: sender.convertToPlayer)
    }
}

extension ViewController: BoardViewDelegate {
    func boardView(_ boardView: BoardView, didSelectCellAt coordinate: Coordinate) {
        guard !animationState.isAnimating else { return }
        guard case .turn(_, let side, let player, _) = store.state.gameProgress else { return }
        guard case .manual = player else { return }
        placeDisk(PlacedDiskCoordinate(disk: side.disk, coordinate: coordinate))
    }
}

ロギング

リファクタリングの手がかりとして状態の変化を都度ログに出力しました。

  • Action, ActionCreatorがdispatchされたときはパラメータも合わせて出力
  • 次のターンになると盤面のデータ状態を出力
- Logic.AppAction.changePlayer(side: sideLight, player: computer) START
▿ Logic.AppPrivateAction.changePlayer
  ▿ changePlayer: (2 elements)
    - side: Logic.Side.sideLight
    - player: Logic.Player.computer
- Logic.AppAction.waitForPlayer() START
- Logic.AppAction.playTurnOfComputer() START
- Logic.AppPrivateAction.startComputerThinking
- Logic.AppAction.playTurnOfComputer() END
- Logic.AppAction.waitForPlayer() END
- Logic.AppAction.saveGame() START
- Logic.AppPrivateAction.finisedSaveGame
- Logic.AppAction.saveGame() END
- Logic.AppAction.changePlayer(side: sideLight, player: computer) END
- Logic.AppAction.nextTurn() from: sideDark to: sideLight
- Logic.AppPrivateAction.nextTurn
- Logic.AppPrivateAction.validateTurn
@01234567
0--------
1--------
2---x----
3---xx---
4---xo---
5--------
6--------
7--------
- Logic.AppAction.waitForPlayer() START
- Logic.AppAction.playTurnOfComputer() START
- Logic.AppPrivateAction.startComputerThinking
- Logic.AppAction.playTurnOfComputer() END
- Logic.AppAction.waitForPlayer() END
- Logic.AppAction.saveGame() START
- Logic.AppPrivateAction.finisedSaveGame
- Logic.AppAction.saveGame() END
- Logic.AppPrivateAction.endComputerThinking
▿ Logic.AppAction.placeDisk
  ▿ placeDisk: Logic.PlacedDiskCoordinate
    - disk: Logic.Disk.diskLight
    ▿ coordinate: Logic.Coordinate
      - x: 2
      - y: 2
- Logic.AppAction.nextTurn() from: sideLight to: sideDark
- Logic.AppPrivateAction.nextTurn
- Logic.AppPrivateAction.validateTurn
@01234567
0--------
1--------
2--ox----
3---ox---
4---xo---
5--------
6--------
7--------
- Logic.AppAction.waitForPlayer() START
- Logic.AppAction.playTurnOfComputer() START
- Logic.AppPrivateAction.startComputerThinking
- Logic.AppAction.playTurnOfComputer() END
- Logic.AppAction.waitForPlayer() END
- Logic.AppAction.saveGame() START
- Logic.AppPrivateAction.finisedSaveGame
- Logic.AppAction.saveGame() END
- Logic.AppPrivateAction.endComputerThinking
▿ Logic.AppAction.placeDisk
  ▿ placeDisk: Logic.PlacedDiskCoordinate
    - disk: Logic.Disk.diskDark
    ▿ coordinate: Logic.Coordinate
      - x: 5
      - y: 4

CI/TEST

GitHub ActionでXcodeのビルドとテストを実施できるようにしました。

#  .github/workflows/build.yml
on:
  push:
    branches:
      - 'master'
  pull_request:
    branches:
      - '**'
env:
  project_nmae: Reversi
  scheme: Reversi
  configuration: Debug

name: Xcode build
jobs:
  validate:
    name: Validate
    runs-on: macOS-latest
    strategy:
      matrix:
        destination:
          - "platform=iOS Simulator,OS=13.4.1,name=iPhone 11 Pro"
    steps:
      - name: Checkout
        uses: actions/checkout@master
      - name: Switch to workspace directory
        run: cd $GITHUB_WORKSPACE
      - name: Install tooling
        run: sudo xcode-select -s /Applications/Xcode_11.4.1.app
      - name: Resolve swift package dependencies
        run: xcodebuild -resolvePackageDependencies -scheme '${{ env.scheme }}' -clonedSourcePackagesDirPath ./.swiftpackages -derivedDataPath ./.build
      - name: Run tests ${{ matrix.destination }}
        run: xcodebuild -sdk iphonesimulator -scheme '${{ env.scheme }}' -configuration '${{ env.configuration }}' -destination '${{ matrix.destination }}' -clonedSourcePackagesDirPath ./.swiftpackages -derivedDataPath ./.build clean test | xcpretty

スクリーンショット 2020-05-16 19.43.22.png

既知の不具合への対処

本チャレンジではリファクタリング対応にとどまらず、潜在する不具合を発見し的確に対処する必要があります。

手詰まりしたときの要対応事象 その1

手詰まりになった場合、アラートが表示された状態でアプリを終了すると、再度アプリを起動して前回終了時をロードしたときにクラッシュします。

スクリーンショット 2020-05-10 1.23.58.png

スクリーンショット 2020-05-10 0.45.39.png

手詰まりしたときの要対応事象 その2

以下のように、ひとまずクラッシュしないように改修すると、今度は再度アプリを起動して前回終了時をロードしたときに手詰まりのアラートが表示されず当該ターンではディスクを置けない状態に陥ります。

diff --git a/Reversi/ViewController.swift b/Reversi/ViewController.swift
index 321824a..411c06b 100644
--- a/Reversi/ViewController.swift
+++ b/Reversi/ViewController.swift
@@ -285,6 +285,7 @@ extension ViewController {
     /// "Computer" が選択されている場合のプレイヤーの行動を決定します。
     func playTurnOfComputer() {
         guard let turn = self.turn else { preconditionFailure() }
+        guard !validMoves(for: turn).isEmpty else { return }
         let (x, y) = validMoves(for: turn).randomElement()!

May-10-2020 01-53-57.gif

リセットアラートの表示時の要対応事象

リセットボタンを押下し、リセットの確認アラートを表示している状態でもコンピュータの操作は可能なのでゲームは進行していきます。このとき、手詰まりが発生すると、手詰まりのアラートを表示すべきところですが、すでにリセットの確認アラートを表示しているためリセットの確認アラートを表示できず操作ができない状態に陥ります。

May-10-2020 01-49-49.gif

リグレッションしないように気を付けるところ

  • 1. コンピュータが思考中にリセットした場合、リセット後のゲームでコンピュータの思考中が継続され誤ってディスクを指さないこと
  • 2. フリップアニメーション中にリセットした場合、リセット後のゲームでフリップアニメーションが継続されないこと
1. 2.
May-16-2020 01-35-13.gif May-10-2020 01-59-13.gif

まとめ

反省点

  • Reduxアーキテクチャに移行にあたって、リファクタリングがおおむね完了するまでテストコードを導入できなかった
    • 移行が完了しないとReduxを活かしたテストコードを導入できなかったため
    • テストコードでリファクタリングの過程にリグレッションしないことを担保したかった
  • リファクタリング前と後でコードステップ数を計測すると67%増加していた
    • Reduxのメリットの堅牢を求めるあまり、複雑なコードになったり過剰なリファクタリングとなった部分が否めない

コードステップ数

- リファクタリング前 リファクタリング後
ステップ数 775 1,295

未完成部分

今後の課題です。

  • テストコードの記述がほとんどありません、もっとテストを書くべき!
  • システム的な異常系のハンドリングはpreconditionFailureassertionFailureの活用にとどまっており、ユーザーに異常状態を通知できていない

感想

リバーシという題材のチョイスがよく程よいボリューム感とリバーシロジックの難易度があり、ベースコードのファトコードの再現具合も絶妙で多様なアーキテクチャでリファクタリングのアプローチが可能なうえ、噛めば噛むほど味の出るすばらしいチャレンジだと感じました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter Web View で Localの HTML , CSSを表示する

はじめに

皆さんはFlutter触ってますか?
Googleが開発しているモバイル向けクロスプラットフォーム,かつWebまでかけちゃうぜ!!ってことで注目しいましたが,思ったとおりにできることも増えてきて,「もうアプリはFlutterで良いじゃん」も近いのかなと思っている今日このごろ.
今回はLocal(端末)に保存したHtml, Css の Web View 表示で手間取ったのでまとめていきます.

Web Viewって?

アプリ内でWebページを表示できる機能です.
アプリによってはWeb Viewだけで実装しているようなのもちらほら...
そんなわけで結構普通に必要な機能です.
今回は端末内に保存したHtmlを表示させることが目的です.
Flutterではこちらのパッケージを使うことで実装できます.

環境

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.15.4 19E287, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 11.4.1)
[✓] Android Studio (version 3.6)
[!] IntelliJ IDEA Ultimate Edition (version 2020.1.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] Connected device (1 available)

導入&問題発生

いつもどおりパッケージを入れて導入しました.
HTMLも/assets/html/view.htmlに配置して,pubspecに追記.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^0.3.21
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_launcher_icons: ^0.7.5
  flutter_test:
    sdk: flutter
  pedantic_mono: any

flutter:
  uses-material-design: true
  assets:
    - assets/icon/
    - assets/html/

flutter_icons:
  android: true
  ios: true
  image_path: 'assets/icon/icon.png'
  adaptive_icon_foreground: 'assets/icon/icon_foreground.png'
  adaptive_icon_background: "#ffa103"

iOSでの設定のために /iOS/Runner/info.plist に追記

info.plist
<key>io.flutter.embedded_views_preview</key>

Mainをこちらを参考に書く.

main.dart
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final String pageTitle = 'Web View Local';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Web View',
      theme: ThemeData(
        primarySwatch: Colors.amber,
      ),
      home: WebViewPage(title: pageTitle),
    );
  }
}

class WebViewPage extends StatefulWidget {
  const WebViewPage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _WebViewState createState() => _WebViewState();
}

class _WebViewState extends State<WebViewPage> {
  WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: WebView(
        onWebViewCreated: (WebViewController webViewController) async {
          // 生成されたWebViewController情報を取得する
          _controller = webViewController;
          // HTMLファイルのURL(ローカルファイルの情報)をControllerに追加する処理
          await _loadHtmlFromAssets();
        },
        // javascriptを有効化
        javascriptMode: JavascriptMode.unrestricted,
      ),
    );
  }

  /// HTMLファイルを読み込む処理(非同期)
  Future _loadHtmlFromAssets() async {
    //HTMLファイルを読み込んでHTML要素を文字列で返す
    final fileText = await rootBundle.loadString('assets/html/view.html');
    await _controller.loadUrl( Uri.dataFromString(
        fileText,
        mimeType: 'text/html',
        encoding: Encoding.getByName('utf-8')
    ).toString());
  }
}

これでDebugしてみたのですが... 動かない!! Androidだけ!!

問題解決

そこでふと思い出したのですが,前にFlutterに学習済みモデル(.tflite)を入れた時,Androidに拡張子を認識させる呪文を唱えた気がします.
そこで物は試しにと /android/app/src/build.gradle に

build.gradle
     :
     :
    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }

    //ここを追記
    aaptOptions {
        noCompress "html"
    }
}

と書いてみると,表示された!!
どうやらapk作成の際に中に html が取り込まれてしまっていたみたいです.
無事解決!!

おわりに

ということでなんとか問題は解決しました.
ちなみに別ファイルに分けていたcssを読み込んでくれない,などのトラブルは有りましたが,これもinline表記することで解決しました.
めでたしめでたし!!

参考資料(ありがとうございます!!)

【Flutter】WebViewを使ってWebサイトとローカルHTMLファイルを表示させる方法
Google公式Flutter用WebViewプラグインを一通り使ってみた
Loading Local Assets in WebView in Flutter

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リリース済みのiosアプリの dSYMファイルの場所(iTunesConnectから取得)

はじめに

FirebaseのCrashlyticsを使っていると、『不足しているオプションの dSYM をアップロードしてください』というメッセージがでることがあると思います。
スクリーンショット 2020-05-16 18.58.44.png

このdSYMファイルの場所を探すのに一苦労したので共有しときます。

※前提として、アプリはリリース済みであるとします。

方法

  • iTunesConnectに移動して、
    [あなたのアプリ]>アクティビティ> [希望するビルド番号]> dSYMのダウンロード
    うんと.png

  • あとはCrashlyticsの指示に従ってdSYMをアップロードしてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】超オススメの「state_notifier + freezed」を使って、カウンターアプリをつくるよ

はじめに

いま、Flutter界隈では、「state_notifier + freezed」を使って開発するのがアツい! と、評判です。

Flutter state_notifierいい感じなので使ったほうが良いですよ
https://qiita.com/_masaokb/items/fe77495db0aeba226d2a
スクリーンショット 2020-05-18 14.42.57.png

「state_notifier」は、providerと組み合わせて使い、Widgetから「状態」と「ロジック」を簡単に分離し&通知することができるライブラリです。Widgetの状態管理を楽にしてくれたり、無駄なリビルドを抑制したりしてくれます。

「freezed」は、State(状態)を「データを保持するだけのクラス」として変更不可(イミュータブル)なクラスとして表現することができます。

Flutter界隈のエンジニアの方々もオススメされているので、「流行ってるみたいだし、めっちゃよさげだし、私も乗り遅れないようにしないと」と思いました。

私も、実際に使ってみて、めちゃくちゃ良かった ですし、オススメ です。

そこで本記事では、「state_notifier + freezed」について、カウンターアプリを作りながら説明します。

スクリーンショット 2020-05-18 14.32.18.png

本記事のコードについて、以下のGitHubリポジトリに公開しています。
https://github.com/karamage/flutter_state_notifier_freezed_sample

state_notifier について

https://pub.dev/packages/state_notifier
スクリーンショット 2020-05-18 14.42.57.png

state_notifier は、providerでの状態管理 を、より簡単に、楽にするライブラリです。

Flutterで、状態管理のパターンとして、以下のものがよく使われています。

  • setState(StatefulWidget)
  • Redux
  • BLoC(Stream + InheritedWidget/Scoped Model)
  • provider(ChangeNotifier)

現時点で、Flutterの状態管理において ベストプラクティスは、providerを使うこと だと思います。
(※個人の感想です。Flutterは進化が早いし、流行り廃りが激しい分野なので、これからどうなるかはワカランです。)
Google公式においても、providerの使用を推奨しています。
(Pragmatic State Management in Flutter (Google I/O'19))

state_notifierは、providerをより使いやすくコードをスッキリと楽に書けるようにしてくれるパッケージです。

従来のproviderパターンにおいて、ChangeNotifierを使ってゴリゴリとコードを書いていたのですが、コードが少々冗長になり、Widget階層が深くなりがちな問題があります。

state_notifierベースでproviderのコードを書くと、
ChangeNotifierを使用した場合よりコードがスッキリし、
コードを書いていて気持ちが良いです。

私は、これまでChangeNotifierベースでコードを書いていたのですが、
これからはstate_notifierベースに書き換えていこうと思います。

freezed について

https://pub.dev/packages/freezed
スクリーンショット 2020-05-18 14.47.04.png

freezedパッケージは、イミュータブルなデータモデルを作成するのに使います。
Stateは、データを保持するだけのクラスにしたいので、 変更不可(イミュータブル/freezed)なクラスにします。
state_notifierでは、State(状態)をイミュータブルなデータモデルとして扱います。
state_notifierとfreezedは相性が良く、セットで使うのが推奨されます。
freezedを使うと、StateにcopyWith(clone)メソッドが自動的に生えるので、便利です。

state_notifier と freezed をパッケージインストール

pubspec.yaml を以下のように書きます。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.0-dev
  state_notifier: ^0.4.0
  flutter_state_notifier: ^0.3.0
  freezed_annotation:

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable:
  build_runner:
  freezed:

json_serializableは、今回のアプリではJSONを扱わないので入れなくても良いのですが、JSONの扱う際にfreezedとセットでいれとくと便利です。

ターミナルで以下のコマンドを叩くと、state_notifier と freezed パッケージのインストールが完了します。

flutter pub get

カウンターアプリを実装する

ここで、flutter createした際のボイラーテンプレートのカウンターアプリを、「state_notifier + freezed」を使用して実装してみます。

Simulator Screen Shot - iPhone 11 Pro - 2020-05-16 at 17.38.36.png

CounterStateとCounterStateNotifierを定義する

新規ファイル「counter_state.dart」 を作成し、以下のコードを記述してください。

counter_state.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier/state_notifier.dart';

part 'counter_state.freezed.dart';
part 'counter_state.g.dart';

@freezed
abstract class CounterState with _$CounterState {
  const factory CounterState({
    @Default(0) int count,
  }) = _CounterState;
  factory CounterState.fromJson(Map<String, dynamic> json) => _$CounterStateFromJson(json);
}

class CounterStateNotifier extends StateNotifier<CounterState> {
  CounterStateNotifier() : super(const CounterState()) {}
  increment() => state = state.copyWith(count: state.count + 1);
}

その後、ターミナルで、以下のコマンドを叩いて、「counter_state.freezed.dart」「counter_state.g.dart」を自動生成してください。

flutter pub pub run build_runner build

counter_state.dart では、「CounterState」クラスと「CounterStateNotifier」クラスを定義しています。

CounterState

CounterStateは、状態データの入れ物で、イミュータブルなデータクラスとして定義します。

カウンターの値の状態変数として「int counter」が定義されています。

デフォルト値を設定するには「@Default(0) int count」のように記述します。

「@freezed」アノテーションをつけることによって、copyWithメソッドが自動で生えます。copyWithメソッドは状態を更新する場合に、新しく状態を生成する(clone)ために使います。

「CounterState.fromJson」メソッドは、今回のカウンターアプリでは使用しませんが、JSONからStateを生成する場合によく使うので記述しています。

CounterStateNotifier

CounterStateNotifierは、状態を操作するロジックを管理し、Widgetに状態の変更を通知します。

MVVMモデルで解釈すると、「ViewModel」に相当します。

「inclement」メソッドは、CouterStateのcountを1カウントアップした状態をcopyWithメソッドを使用して、新しく状態を作成し、更新しています。

CounterStateNotifierを使用したWidgetを作成する

main.dartに以下のコードを記述します。

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'package:state_notifier_example/counter_state.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StateNotifierProvider<CounterStateNotifier, CounterState>(
        create: (_) => CounterStateNotifier(),
        child: HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('state_notifier sample'),
      ),
      body: Center(
        child: Text(
          context.select<CounterState, int>((state) => state.count).toString(),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => context.read<CounterStateNotifier>().increment(),
        label: Text('1'),
        icon: Icon(Icons.add),
      ),
    );
  }
}

Widgetツリーの上層で、StateNotifierProviderを作成すれば、その子Widgetであればどこでも、CounterStateNotifierを取得することができます。

ChangeNotifierを使った場合、Consumer等を挟まなくてはいけなかったのですが、state_notifierの場合スッキリ書けます。

main.dart
      home: StateNotifierProvider<CounterStateNotifier, CounterState>(
        create: (_) => CounterStateNotifier(),
        child: HomePage(),
      ),

CounterStateNotifierを取得したい場合、以下のようにすれば、取得できます。

main.dart
context.select<CounterState, int>((state) => state.count).toString(),

...

onPressed: () => context.read<CounterStateNotifier>().increment(),

context.readとcontext.selectには、以下の違いがあります。

  • context.read<CounterStateNotifier>()

    • State更新時リビルドしない
    • StateNotifierのメソッドを使いたいときに使う
  • context.select<CounterState, int>()

    • State更新時リビルドする
    • 状態が変わったときに画面に反映したい場合に使う

おわりに

「state_notifier」を使用することにより、簡単にWidgetから「状態」と「ロジック」を分離することができました。
「freezed」を使用することにより、Stateをデータを保持するだけのクラス、 変更不可(イミュータブル/freezed)なクラスにすることができました。

コードがきれいでスッキリして、好きです。

超オススメですので、「state_notifier + freezed」を使ってみてはいかがでしょうか。

本記事のコードについて、以下のGitHubリポジトリに公開しています。
https://github.com/karamage/flutter_state_notifier_freezed_sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TwitterのプロフィールであるようなUITableViewのヘッダーの画像を引っ張って拡大させる機能をたった数行のコードで実装する

Tiwtter等のアプリでよくあるプロフィール画面のカバー画像を下に引っ張ると拡大されるPrallaxHeader機能(っていうんですかね?)ですが、
実はこれめちゃくちゃ簡単に実装できます。

完成系はこんな感じです。
ezgif-7-b4b7b38acba0.gif

GitHubにサンプルもあります。

環境

Xcode Version 11.4.1 (11E503a)
Swift version 5.2.2 (swiftlang-1103.0.32.6 clang-1103.0.32.51).

実装

使用しているはUITableViewとtableHeaderViewです。
まあこの構成はよくあると思いますが、ポイントはヘッダー画像をtableHeaderViewに入れないことです。

DebugViewHierarchyでみるとよくわかると思います。
スクリーンショット 2020-05-16 14.44.34.png

Interface Builder

  • UIViewController.View
    • UIImageView ←これがTwitterでいう下に引っ張ると拡大されるカバー画像になる。
    • UITableView ←透明にする。
    • UIView ←これがtableHeaderView
      • UIView ←これがカバーイメージの上にかぶる部分。透明にする。
      • UIView ←ヘッダーに載せたいコンテンツを載せるView。背景色をつける。
      • UILabel ←ヘッダーに載せたいコンテンツ。今回は適当にラベル。

上記の構成でViewを配置し、カバー画像のUIImageView下記のAutoLayoutをかけます。
- Align Top to SuperView Top
- Align Center X
- 任意のAspect Ratio
- Height Equal = 適当な数値

カバーイメージの上にかぶる部分の透明なViewに下記のAutoLayoutをかけます。
- Align Top to SuperView Top
- Trailing Space To SuperView
- Leading Space To SuperView
- カバー画像と同じAspect Ratio

あとは実装したいUIにあわせます。

コード

カバー画像Viewの Align Top to SuperView Top- Height Equal = 適当な数値 をIBOutletで接続します。 

@IBOutlet private var coverImageHeightConstraint: NSLayoutConstraint!
@IBOutlet private var coverImageTopConstraint: NSLayoutConstraint!

- Height Equal = 適当な数値のままだと高さがずれるのでデフォルトの高さをセットしてあげてください。

private var defaultCoverImageHeight: CGFloat { return self.view.frame.width / 3 }

override func viewDidLoad() {
    super.viewDidLoad()

    self.coverImageHeightConstraint.constant = self.defaultCoverImageHeight
}

最後に、UITableViewDelegate.scrollViewDidScroll(_ scrollView: UIScrollView)の中にcontentsOffset.yの変化に応じて適当な値をIBOutletで接続した制約に与えてあげれば終わりです。

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.coverImageHeightConstraint.constant = max(self.defaultCoverImageHeight, -(scrollView.contentOffset.y - self.defaultCoverImageHeight))
        self.coverImageTopConstraint.constant = min(0, -scrollView.contentOffset.y)
    }
}

おわり

UITableViewの下にUIImageViewを配置し、必要な箇所の背景色を透明にして制約をいじるだけなので割と既存の実装に反映させやすいと思います。
また、スクロール位置に合わせてブラーを追加等も機能も簡単に追加できると思うのでおすすめです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftでTwitterのプロフィールであるようなUITableViewのヘッダーの画像を引っ張って拡大させる機能をたった数行のコードで実装する

Tiwtter等のアプリでよくあるプロフィール画面のカバー画像を下に引っ張ると拡大されるPrallaxHeader機能(っていうんですかね?)ですが、
実はこれめちゃくちゃ簡単に実装できます。

完成系はこんな感じです。
ezgif-7-b4b7b38acba0.gif

GitHubにサンプルもあります。

環境

Xcode Version 11.4.1 (11E503a)
Swift version 5.2.2 (swiftlang-1103.0.32.6 clang-1103.0.32.51).

実装

使用しているはUITableViewとtableHeaderViewです。
まあこの構成はよくあると思いますが、ポイントはヘッダー画像をtableHeaderViewに入れないことです。

DebugViewHierarchyでみるとよくわかると思います。
スクリーンショット 2020-05-16 14.44.34.png

Interface Builder

  • UIViewController.View
    • UIImageView ←これがTwitterでいう下に引っ張ると拡大されるカバー画像になる。
    • UITableView ←透明にする。
    • UIView ←これがtableHeaderView
      • UIView ←これがカバーイメージの上にかぶる部分。透明にする。
      • UIView ←ヘッダーに載せたいコンテンツを載せるView。背景色をつける。
      • UILabel ←ヘッダーに載せたいコンテンツ。今回は適当にラベル。

上記の構成でViewを配置し、カバー画像のUIImageView下記のAutoLayoutをかけます。
- Align Top to SuperView Top
- Align Center X
- 任意のAspect Ratio
- Height Equal = 適当な数値

カバーイメージの上にかぶる部分の透明なViewに下記のAutoLayoutをかけます。
- Align Top to SuperView Top
- Trailing Space To SuperView
- Leading Space To SuperView
- カバー画像と同じAspect Ratio

あとは実装したいUIにあわせます。

コード

カバー画像Viewの Align Top to SuperView Top- Height Equal = 適当な数値 をIBOutletで接続します。 

@IBOutlet private var coverImageHeightConstraint: NSLayoutConstraint!
@IBOutlet private var coverImageTopConstraint: NSLayoutConstraint!

- Height Equal = 適当な数値のままだと高さがずれるのでデフォルトの高さをセットしてあげてください。

private var defaultCoverImageHeight: CGFloat { return self.view.frame.width / 3 }

override func viewDidLoad() {
    super.viewDidLoad()

    self.coverImageHeightConstraint.constant = self.defaultCoverImageHeight
}

最後に、UITableViewDelegate.scrollViewDidScroll(_ scrollView: UIScrollView)の中にcontentsOffset.yの変化に応じて適当な値をIBOutletで接続した制約に与えてあげれば終わりです。

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.coverImageHeightConstraint.constant = max(self.defaultCoverImageHeight, -(scrollView.contentOffset.y - self.defaultCoverImageHeight))
        self.coverImageTopConstraint.constant = min(0, -scrollView.contentOffset.y)
    }
}

おわり

UITableViewの下にUIImageViewを配置し、必要な箇所の背景色を透明にして制約をいじるだけなので割と既存の実装に反映させやすいと思います。
また、スクロール位置に合わせてブラーを追加する機能等も簡単に追加できると思うのでおすすめです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超爆速 秒でコーディング】【Flutter】Adobe 公式 XD プラグインでXDからFlutterに変換してみた

XD→Flutterがやばい

この変換が30秒でできます。

ファイル名

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

ファイル名 ファイル名

AdobeがXDのプラグインによるFlutterの公式サポート開始

昨年のFlutter Interactイベントでは、Adobe から、Flutterコードをツールから直接エクスポートするAdobe XDのプラグインの初期プロトタイプのデモンストレーションが発表されました。
adobe公式ブログ
YouTubeに掲載されている昨年のFlutter Interact

2020年05月14日より、AdobeのXD to Flutterプラグインがより広範なパブリックテストの早期アクセスとして利用できるようになりました。

→ プラグインのインストールリンクはこちら

XD→Flutter変換方法

変換するXDはこちら

image.png

プラグインをインストール後にXDからPluginを確認すると以下のような画面が表示されます。

Screen Shot 2020-05-16 at 1.13.59 AM.png

FLUTTER PROJECT

Flutter projectの項目にパスを設定すると以下のエラーが発生します。

Could not find dependencies entry in pubspec.yaml for:adobe_xd.

image.png

以下を追記して、Flutterにadobe_xdパッケージを導入することでエラーは解決しました。

Pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  adobe_xd:

CODE PATH

通常はlibを選択すれば問題ありません。

IMAGE PATH

画像を保存するパスを指定します。

私の場合は以下のディレクトリ構成のため、assets/imagesを指定しました。

image.png

WIDGET NAME PREFIX

自動生成されるDartファイルのプリフィックスになります。
{WIDGET NAME PREFIX}{アートボード名}が生成されます.

Export All Widgetsを押下

実際に生成されたのがこちら

  • assets/images/skylar-em-mybmb.jpg
  • XD_list_screen.dart
  • XD_play_screen.dart

image.png

FlutterをRunしてみたらこんな感じ

image.pngimage.png

生成されたDartファイルはこんな感じ

XD_play_screen.dart
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter_svg/flutter_svg.dart';

class XD_play_screen extends StatelessWidget {
  XD_play_screen({
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffe8ecef),
      body: Stack(
        children: <Widget>[
          Transform.translate(
            offset: Offset(-52.97, -70.64),
            child:
                // Adobe XD layer: 'back_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(75.27, 98.82),
                  child: Container(
                    width: 40.0,
                    height: 41.3,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.65)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(75.27, 98.82),
                  child: Container(
                    width: 40.0,
                    height: 41.3,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.65)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(88.97, 114.37),
                  child:
                      // Adobe XD layer: 'Backward arrow' (group)
                      SvgPicture.string(
                    _shapeSVG_383546ed0a284f858ac75e39984bace7,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(-113.97, 232.36),
            child:
                // Adobe XD layer: 'prev_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(201.02, 454.97),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(201.02, 454.97),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(216.11, 464.71),
                  child: SvgPicture.string(
                    _shapeSVG_cc8eebda6f964eb89a9d866fa47aad1d,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(-65.97, 262.36),
            child:
                // Adobe XD layer: 'next_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(312.89, 424.99),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(312.89, 424.99),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(321.02, 434.79),
                  child: SvgPicture.string(
                    _shapeSVG_ee800ac22d2b4193b83a78fb44bc30c0,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(63.03, -24.64),
            child:
                // Adobe XD layer: 'menu_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(245.37, 53.11),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(245.37, 53.11),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(261.14, 69.52),
                  child:
                      // Adobe XD layer: 'Menu' (group)
                      SvgPicture.string(
                    _shapeSVG_91dee9b001434d16b9d8288df49420ed,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
                Transform.translate(
                  offset: Offset(261.14, 68.56),
                  child:
                      // Adobe XD layer: 'Menu' (group)
                      Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 10.5,
                          height: 10.5,
                          decoration: BoxDecoration(),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(-90.97, 246.36),
            child:
                // Adobe XD layer: 'stop_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(247.99, 431.35),
                  child: Container(
                    width: 60.0,
                    height: 60.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(30.0, 30.0)),
                      gradient: LinearGradient(
                        begin: Alignment(0.0, -1.0),
                        end: Alignment(0.0, 1.0),
                        colors: [
                          const Color(0xffff00c4),
                          const Color(0xff000000)
                        ],
                        stops: [0.0, 1.0],
                      ),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x5e000000),
                            offset: Offset(3, 3),
                            blurRadius: 6)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(271.58, 450.51),
                  child: SvgPicture.string(
                    _shapeSVG_aec0aade47ee4ca5b903f27d7de86884,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(22.29, 566.95),
            child: SvgPicture.string(
              _shapeSVG_25f81fa21ab349f89461efc916412212,
              allowDrawingOutsideViewBox: true,
            ),
          ),
          Transform.translate(
            offset: Offset(-48.21, 204.45),
            child: Stack(
              children: <Widget>[],
            ),
          ),
          Transform.translate(
            offset: Offset(22.0, 601.0),
            child: Text(
              '00.00',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 18,
                color: const Color(0xff898c8d),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
          Transform.translate(
            offset: Offset(302.0, 601.0),
            child: Text(
              '05.36',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 18,
                color: const Color(0xff898c8d),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
          Transform.translate(
            offset: Offset(315.0, 646.0),
            child:
                // Adobe XD layer: 'Merge' (group)
                Stack(
              children: <Widget>[
                Container(
                  width: 20.0,
                  height: 20.0,
                  decoration: BoxDecoration(),
                ),
                Transform.translate(
                  offset: Offset(0.0, 1.49),
                  child: SvgPicture.string(
                    _shapeSVG_00696ad8541d467faa4d8ea05320fccc,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(35.0, 646.0),
            child:
                // Adobe XD layer: 'Refresh' (group)
                SvgPicture.string(
              _shapeSVG_ecc8a9fa818245168f8aef45d5432b1b,
              allowDrawingOutsideViewBox: true,
            ),
          ),
          Transform.translate(
            offset: Offset(63.0, 169.79),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(-101.1, 0.0),
                  child:
                      // Adobe XD layer: 'skylar-em-mybmb' (shape)
                      Container(
                    width: 438.1,
                    height: 246.4,
                    decoration: BoxDecoration(
                      image: DecorationImage(
                        image: const AssetImage(
                            'assets/images/skylar-em-mybmb.jpg'),
                        fit: BoxFit.fill,
                      ),
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Container(
                    width: 248.0,
                    height: 248.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(124.02, 124.02)),
                      color: const Color(0xffffffff),
                      border: Border.all(
                          width: 1.0, color: const Color(0xff707070)),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(110.88, 422.89),
            child: Text(
              'Leaving Heaven',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 18,
                color: const Color(0xff000000),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
          Transform.translate(
            offset: Offset(155.95, 453.0),
            child: Text(
              'Eminem',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 14,
                color: const Color(0x94000000),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
        ],
      ),
    );
  }
}

const String _shapeSVG_383546ed0a284f858ac75e39984bace7 =
    '<svg viewBox="89.0 114.4 13.6 13.6" ><g transform="translate(88.97, 114.37)"><path transform="matrix(-1.0, 0.0, 0.0, -1.0, 13.61, 13.61)" d="M 6.803047180175781 0 L 5.5661301612854 1.236917734146118 L 10.24874687194824 5.919534683227539 L 0 5.919534683227539 L 1.024800016592356e-16 7.686559677124023 L 10.24874687194824 7.686559677124023 L 5.5661301612854 12.36917686462402 L 6.803047180175781 13.60609436035156 L 13.60609436035156 6.803047180175781 L 6.803047180175781 0 Z" fill="#000000" fill-opacity="0.74" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_cc8eebda6f964eb89a9d866fa47aad1d =
    '<svg viewBox="216.1 464.7 16.9 16.3" ><g transform="translate(216.11, 464.71)"><path transform="matrix(0.866025, 0.5, -0.5, 0.866025, 5.72, 0.0)" d="M 6.432093620300293 0 L 12.8641881942749 11.43483924865723 L 0 11.43483924865723 Z" fill="#920070" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(0.44, 2.65)" d="M 0 0 L 8.881784197001252e-16 13.64728927612305" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_ee800ac22d2b4193b83a78fb44bc30c0 =
    '<svg viewBox="321.0 434.8 16.9 16.7" ><g transform="translate(321.02, 434.79)"><path transform="matrix(0.866025, -0.5, 0.5, 0.866025, 0.0, 6.43)" d="M 6.432093620300293 0 L 12.8641881942749 11.43483924865723 L 0 11.43483924865723 Z" fill="#920070" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(16.42, 3.04)" d="M 0 0 L 8.881784197001252e-16 13.64728927612305" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_91dee9b001434d16b9d8288df49420ed =
    '<svg viewBox="261.1 69.5 10.5 10.5" ><g transform="translate(261.14, 68.56)"><path transform="translate(0.0, 0.96)" d="M 6.106226635438361e-16 10.52874183654785 L 5.233715995921835e-16 9.024304389953613 L 6.580931663513184 9.024304389953613 L 6.580931663513184 10.52874183654785 L 6.106226635438361e-16 10.52874183654785 Z M 3.489369166873035e-16 6.01659107208252 L 2.616857997960918e-16 4.512152194976807 L 10.52874183654785 4.512152194976807 L 10.52874183654785 6.01659107208252 L 3.489369166873035e-16 6.01659107208252 Z M 8.725107718654236e-17 1.504438281059265 L 0 0 L 10.52874183654785 6.106226635438361e-16 L 10.52874183654785 1.504438281059265 L 8.725107718654236e-17 1.504438281059265 Z" fill="#000000" fill-opacity="0.74" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_aec0aade47ee4ca5b903f27d7de86884 =
    '<svg viewBox="271.6 450.5 11.1 20.5" ><g transform="translate(271.58, 450.51)"><path transform="translate(0.0, 0.0)" d="M 0 0 L 1.188911008996542e-15 20.49995613098145" fill="none" stroke="#ffffff" stroke-width="3" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(11.15, 0.0)" d="M 0 0 L 1.165734175856414e-15 20.49995613098145" fill="none" stroke="#ffffff" stroke-width="3" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_25f81fa21ab349f89461efc916412212 =
    '<svg viewBox="22.3 566.9 326.1 29.4" ><g transform="translate(-48.21, 204.45)"><g transform="translate(70.5, 362.5)"><path transform="translate(0.0, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(6.94, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(13.88, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(20.81, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(27.75, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(34.69, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(41.63, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(48.57, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(55.51, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(62.44, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(69.38, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(76.32, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(83.26, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(90.2, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(97.14, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(104.07, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /></g><g transform="translate(181.51, 362.5)"><path transform="translate(0.0, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(6.94, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(13.88, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(20.81, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(27.75, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(34.69, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(41.63, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(48.57, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(55.51, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(62.44, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(69.38, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(76.32, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(83.26, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(90.2, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(97.14, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(104.07, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#920070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /></g><g transform="translate(292.53, 362.5)"><path transform="translate(0.0, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(6.94, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(13.88, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(20.81, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(27.75, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(34.69, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(41.63, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(48.57, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(55.51, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(62.44, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(69.38, 8.4)" d="M 0 0 L 0 12.60015106201172" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(76.32, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(83.26, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(90.2, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(97.14, 0.0)" d="M 0 0 L 0 29.40035057067871" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(104.07, 4.2)" d="M 0 0 L 0 21.00025177001953" fill="none" stroke="#707070" stroke-width="1.5" stroke-miterlimit="4" stroke-linecap="butt" /></g></g></svg>';
const String _shapeSVG_00696ad8541d467faa4d8ea05320fccc =
    '<svg viewBox="0.0 1.5 19.9 16.4" ><path transform="translate(0.0, 1.49)" d="M 15.23306465148926 14.06151008605957 L 13.47568225860596 14.06151008605957 C 11.25159454345703 14.06151008605957 9.25841236114502 12.77399349212646 8.203941345214844 10.77989292144775 L 6.09520435333252 6.675831317901611 C 5.507744312286377 5.391578197479248 4.217473983764648 4.686625957489014 2.927101612091064 4.686625957489014 L 0 4.686625957489014 L 0 2.344944953918457 L 2.927101612091064 2.344944953918457 C 5.156288146972656 2.344944953918457 7.149572849273682 3.634093523025513 8.203941345214844 5.628194332122803 L 10.31267833709717 9.728992462158203 C 10.89503955841064 11.01324462890625 12.18541145324707 11.71819686889648 13.47568225860596 11.71819686889648 L 15.23306465148926 11.71819686889648 L 15.23306465148926 9.373251914978027 L 19.92254638671875 12.89311695098877 L 15.23306465148926 16.40645599365234 L 15.23306465148926 14.06151008605957 Z M 0 14.06151008605957 L 0 11.71819686889648 L 2.927101612091064 11.71819686889648 C 3.981470108032227 11.71819686889648 4.804831981658936 11.36735248565674 5.507744312286377 10.66240119934082 L 5.974754810333252 11.83405780792236 C 6.210656642913818 12.18979740142822 6.326108932495117 12.53574562072754 6.562214851379395 12.77399349212646 C 5.623298645019531 13.59480571746826 4.332926273345947 14.06151008605957 2.927101612091064 14.06151008605957 L 0 14.06151008605957 Z M 15.23306465148926 4.686625957489014 L 13.47568225860596 4.686625957489014 C 12.42141532897949 4.686625957489014 11.60305118560791 5.039102077484131 10.89503955841064 5.742422580718994 L 10.42823314666748 4.572397708892822 C 10.1921272277832 4.218289852142334 10.07667446136475 3.870709419250488 9.84077262878418 3.634093523025513 C 10.77958679199219 2.811649322509766 12.06995964050293 2.344944953918457 13.47568225860596 2.344944953918457 L 15.23306465148926 2.344944953918457 L 15.23306465148926 0 L 19.92254638671875 3.511705875396729 L 15.23306465148926 7.033202648162842 L 15.23306465148926 4.686625957489014 Z" fill="#898c8d" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></svg>';
const String _shapeSVG_ecc8a9fa818245168f8aef45d5432b1b =
    '<svg viewBox="35.0 646.0 20.0 20.0" ><g transform="translate(35.0, 646.0)"><path  d="M 6.25 12.5 C 6.875 12.5 7.125000476837158 12.875 6.625000476837158 13.375 L 4.750000476837158 15.25 C 6.125 16.62500190734863 8.000000953674316 17.5 10 17.5 C 13.875 17.5 17 14.625 17.5 11 C 17.5 11 17.625 10 18.75 10 C 19.5 10 20 10.5 20 11.25 C 20 11.37500095367432 20 11.37500095367432 20 11.5 C 19.25 16.375 15.125 20 10 20 C 7.25 20 4.750000476837158 18.875 2.875000238418579 17.125 L 0.8750001788139343 19.12500190734863 C 0.3750000298023224 19.625 0 19.375 0 18.75 L 0 13.125 C 0 12.75 0.2500000298023224 12.5 0.625 12.5 L 6.25 12.5 Z M 13.75 7.5 C 13.125 7.5 12.875 7.125000476837158 13.375 6.625000476837158 L 15.25 4.750000476837158 C 13.875 3.375000238418579 12.00000095367432 2.5 10 2.5 C 6.125 2.5 3.000000238418579 5.375000476837158 2.5 9 C 2.5 9 2.375 10 1.25 10 C 0.5000000596046448 10 0 9.5 0 8.75 C 0 8.625000953674316 0 8.625000953674316 0 8.5 C 0.7500000596046448 3.625 4.875 0 10 0 C 12.75 0 15.25 1.125 17.125 2.875 L 19.12500190734863 0.875 C 19.625 0.3750000298023224 20 0.625 20 1.25 L 20 6.875 C 20 7.25 19.75000190734863 7.5 19.375 7.5 L 13.75 7.5 Z" fill="#898c8d" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';

XD_list_screen.dart
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter_svg/flutter_svg.dart';
import 'package:adobe_xd/specific_rect_clip.dart';

class XD_list_screen extends StatelessWidget {
  XD_list_screen({
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffe8ecef),
      body: Stack(
        children: <Widget>[
          Transform.translate(
            offset: Offset(-52.97, -70.64),
            child:
                // Adobe XD layer: 'back_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(75.27, 98.82),
                  child: Container(
                    width: 40.0,
                    height: 41.3,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.65)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(75.27, 98.82),
                  child: Container(
                    width: 40.0,
                    height: 41.3,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.65)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(88.97, 114.37),
                  child:
                      // Adobe XD layer: 'Backward arrow' (group)
                      SvgPicture.string(
                    _shapeSVG_96eb9fb5134542799a4150a6bd089216,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(63.03, -24.64),
            child:
                // Adobe XD layer: 'menu_btn' (group)
                Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(245.37, 53.11),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x96ffffff),
                            offset: Offset(-2, -2),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(245.37, 53.11),
                  child: Container(
                    width: 40.0,
                    height: 40.0,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(20.0, 20.0)),
                      color: const Color(0xffebecf0),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x24000000),
                            offset: Offset(3, 3),
                            blurRadius: 1.5)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(261.14, 69.52),
                  child:
                      // Adobe XD layer: 'Menu' (group)
                      SvgPicture.string(
                    _shapeSVG_020664c98c7f40988eec662dd04f1eaa,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
                Transform.translate(
                  offset: Offset(261.14, 68.56),
                  child:
                      // Adobe XD layer: 'Menu' (group)
                      Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 10.5,
                          height: 10.5,
                          decoration: BoxDecoration(),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(116.0, 105.79),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(-58.41, 0.0),
                  child:
                      // Adobe XD layer: 'skylar-em-mybmb' (shape)
                      Container(
                    width: 253.1,
                    height: 142.4,
                    decoration: BoxDecoration(
                      image: DecorationImage(
                        image: const AssetImage(
                            'assets/images/skylar-em-mybmb.jpg'),
                        fit: BoxFit.fill,
                      ),
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Container(
                    width: 143.3,
                    height: 143.3,
                    decoration: BoxDecoration(
                      borderRadius:
                          BorderRadius.all(Radius.elliptical(71.65, 71.65)),
                      color: const Color(0xffffffff),
                      border: Border.all(
                          width: 1.0, color: const Color(0xff707070)),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(118.88, 28.89),
            child: Text(
              'Leaving Heaven',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 18,
                color: const Color(0xff000000),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
          Transform.translate(
            offset: Offset(163.95, 59.0),
            child: Text(
              'Eminem',
              style: TextStyle(
                fontFamily: 'Open Sans',
                fontSize: 14,
                color: const Color(0x94000000),
                fontWeight: FontWeight.w600,
              ),
              textAlign: TextAlign.left,
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 299.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_76d2aedc11af4dc482a870903997dcff,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 369.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 439.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_bce56cc9a5f84e1aa05ddfa22e0b2068,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 509.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_ed54718f34e74e9dac15b29107fa6038,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 579.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_676e0afef6c443d280271cbb62d129df,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 649.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_4896be0f759c4917bc911be2e9723cf9,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(21.0, 719.0),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(0.0, 0.0),
                  child: Text(
                    'Rap God',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 19,
                      color: const Color(0xff000000),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(0.0, 31.0),
                  child: Text(
                    'Eminem',
                    style: TextStyle(
                      fontFamily: 'Open Sans',
                      fontSize: 14,
                      color: const Color(0xff616364),
                      fontWeight: FontWeight.w600,
                    ),
                    textAlign: TextAlign.left,
                  ),
                ),
                Transform.translate(
                  offset: Offset(297.38, 6.2),
                  child: Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x96ffffff),
                                  offset: Offset(-2, -2),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(0.0, 0.0),
                        child: Container(
                          width: 35.6,
                          height: 35.6,
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.all(
                                Radius.elliptical(17.81, 17.81)),
                            color: const Color(0xffebecf0),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x24000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 1.5)
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
                Transform.translate(
                  offset: Offset(309.77, 13.94),
                  child: SvgPicture.string(
                    _shapeSVG_8ed41034be1b456fa5327cb28a2be403,
                    allowDrawingOutsideViewBox: true,
                  ),
                ),
              ],
            ),
          ),
          Transform.translate(
            offset: Offset(-49.93, -393.81),
            child: Stack(
              children: <Widget>[
                Transform.translate(
                  offset: Offset(56.93, 751.81),
                  child: Container(
                    width: 359.0,
                    height: 74.0,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(16.0),
                      border: Border.all(
                          width: 1.0, color: const Color(0xffe8ecef)),
                      boxShadow: [
                        BoxShadow(
                            color: const Color(0x45000000),
                            offset: Offset(1, 1),
                            blurRadius: 1)
                      ],
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(54.43, 749.31),
                  child: SpecificRectClip(
                    rect: Rect.fromLTWH(0, 0, 362, 77),
                    child: UnconstrainedBox(
                      alignment: Alignment.topLeft,
                      child: Container(
                        width: 362,
                        height: 77,
                        child: GridView.count(
                          primary: false,
                          padding: EdgeInsets.all(0),
                          mainAxisSpacing: 20,
                          crossAxisSpacing: 20,
                          crossAxisCount: 1,
                          childAspectRatio: 4.701298701298701,
                          children: [
                            {},
                          ].map((map) {
                            return Transform.translate(
                              offset: Offset(-54.43, -749.31),
                              child: Stack(
                                children: <Widget>[
                                  Transform.translate(
                                    offset: Offset(56.93, 751.81),
                                    child: Container(
                                      width: 359.0,
                                      height: 74.0,
                                      decoration: BoxDecoration(
                                        borderRadius:
                                            BorderRadius.circular(16.0),
                                        border: Border.all(
                                            width: 1.0,
                                            color: const Color(0xffe8ecef)),
                                        boxShadow: [
                                          BoxShadow(
                                              color: const Color(0x45ffffff),
                                              offset: Offset(-1, -1),
                                              blurRadius: 1)
                                        ],
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            );
                          }).toList(),
                        ),
                      ),
                    ),
                  ),
                ),
                Transform.translate(
                  offset: Offset(120.96, 340.17),
                  child:
                      // Adobe XD layer: 'stop_btn' (group)
                      Stack(
                    children: <Widget>[
                      Transform.translate(
                        offset: Offset(247.99, 431.35),
                        child: Container(
                          width: 35.0,
                          height: 35.0,
                          decoration: BoxDecoration(
                            borderRadius:
                                BorderRadius.all(Radius.elliptical(17.5, 17.5)),
                            gradient: LinearGradient(
                              begin: Alignment(0.0, -1.0),
                              end: Alignment(0.0, 1.0),
                              colors: [
                                const Color(0xffff00c4),
                                const Color(0xff000000)
                              ],
                              stops: [0.0, 1.0],
                            ),
                            boxShadow: [
                              BoxShadow(
                                  color: const Color(0x5e000000),
                                  offset: Offset(3, 3),
                                  blurRadius: 6)
                            ],
                          ),
                        ),
                      ),
                      Transform.translate(
                        offset: Offset(261.63, 442.3),
                        child: SvgPicture.string(
                          _shapeSVG_7183cc78301f48a287bb50591c2bf5cd,
                          allowDrawingOutsideViewBox: true,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

const String _shapeSVG_96eb9fb5134542799a4150a6bd089216 =
    '<svg viewBox="89.0 114.4 13.6 13.6" ><g transform="translate(88.97, 114.37)"><path transform="matrix(-1.0, 0.0, 0.0, -1.0, 13.61, 13.61)" d="M 6.803047180175781 0 L 5.5661301612854 1.236917734146118 L 10.24874687194824 5.919534683227539 L 0 5.919534683227539 L 1.024800016592356e-16 7.686559677124023 L 10.24874687194824 7.686559677124023 L 5.5661301612854 12.36917686462402 L 6.803047180175781 13.60609436035156 L 13.60609436035156 6.803047180175781 L 6.803047180175781 0 Z" fill="#000000" fill-opacity="0.74" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_020664c98c7f40988eec662dd04f1eaa =
    '<svg viewBox="261.1 69.5 10.5 10.5" ><g transform="translate(261.14, 68.56)"><path transform="translate(0.0, 0.96)" d="M 6.106226635438361e-16 10.52874183654785 L 5.233715995921835e-16 9.024304389953613 L 6.580931663513184 9.024304389953613 L 6.580931663513184 10.52874183654785 L 6.106226635438361e-16 10.52874183654785 Z M 3.489369166873035e-16 6.01659107208252 L 2.616857997960918e-16 4.512152194976807 L 10.52874183654785 4.512152194976807 L 10.52874183654785 6.01659107208252 L 3.489369166873035e-16 6.01659107208252 Z M 8.725107718654236e-17 1.504438281059265 L 0 0 L 10.52874183654785 6.106226635438361e-16 L 10.52874183654785 1.504438281059265 L 8.725107718654236e-17 1.504438281059265 Z" fill="#000000" fill-opacity="0.74" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';
const String _shapeSVG_76d2aedc11af4dc482a870903997dcff =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_bce56cc9a5f84e1aa05ddfa22e0b2068 =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_ed54718f34e74e9dac15b29107fa6038 =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_676e0afef6c443d280271cbb62d129df =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_4896be0f759c4917bc911be2e9723cf9 =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_8ed41034be1b456fa5327cb28a2be403 =
    '<svg viewBox="309.8 13.9 12.4 20.1" ><defs><filter id="shadow"><feDropShadow dx="3" dy="3" stdDeviation="6"/></filter></defs><path transform="matrix(0.0, 1.0, -1.0, 0.0, 322.16, 13.94)" d="M 10.06744003295898 0 L 20.13488388061523 12.39069938659668 L 0 12.39069938659668 Z" fill="#373737" stroke="none" stroke-width="1" stroke-miterlimit="4" stroke-linecap="butt" filter="url(#shadow)"/></svg>';
const String _shapeSVG_7183cc78301f48a287bb50591c2bf5cd =
    '<svg viewBox="261.6 442.3 6.8 12.4" ><g transform="translate(261.63, 442.3)"><path transform="translate(0.0, 0.0)" d="M 0 0 L 1.188911008996542e-15 12.42906475067139" fill="none" stroke="#ffffff" stroke-width="3" stroke-miterlimit="4" stroke-linecap="butt" /><path transform="translate(6.76, 0.0)" d="M 0 0 L 1.165734175856414e-15 12.42906475067139" fill="none" stroke="#ffffff" stroke-width="3" stroke-miterlimit="4" stroke-linecap="butt" /></g></svg>';

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む