$30 off During Our Annual Pro Sale. View Details »

ゆめみの Flutter エンジニア育成方法

Kanta Mori
November 16, 2023

ゆめみの Flutter エンジニア育成方法

Kanta Mori

November 16, 2023
Tweet

More Decks by Kanta Mori

Other Decks in Programming

Transcript

  1. INDEX 1. 基礎的なレビュー観点 2. 各セッションの紹介 3.終わりに Session0: Setup Session1: Layout

    Session2: API Session3: Lifecycle Session4: Mixin Session5: Error Session6: JSON Session7: Serialization Session8: StateManagement Session9: UnitTest Session10: WidgetTest Session11: ThreadBlock
  2. Dartの基礎的な部分 • final と const を適切に利⽤しているか • const をつけ忘れていないか •

    nullable な 変数を適切に利⽤しているか 基礎的なレビュー項⽬
  3. Dartの基礎的な部分 • final と const を適切に利⽤しているか • const をつけ忘れていないか •

    nullable な 変数を適切に利⽤しているか 基礎的なレビュー項⽬
  4. • 実⾏結果 ハッシュ値 518843245 164395217 997697555 254820803 318602242 550760522 550760522

    550760522 550760522 550760522 コンパイル時定数のパフォーマンス 基礎的なレビュー項⽬ const なし const あり
  5. GitHub の基礎的な部分 • コミット粒度が適切か • コミット⽂が適切か • プルリクエストを提出してレビュアーがレビューし始めた後で、force puth を使⽤していないか

    • レビュー後に UI が変更された場合、PR 添付の画像も変更されているか • ⾃動⽣成ファイルのコミット状況に応じて README や CI が適切に設定され ているか 基礎的なレビュー項⽬
  6. プログラミングの基礎的な部分 • 保守性 ◦ ハードコーディング ◦ テスタブルなコード • 安全性 ◦

    型安全 ◦ メモリ安全 • 可読性 ◦ 早期リターン ◦ 説明変数 基礎的なレビュー項⽬
  7. 課題の準備をする • GitHub Repository の権限を設定 ◦ レビュアー招待 ◦ ワークフロー ◦

    ブランチ保護 • 開発環境を構築 ◦ FVM(Flutter Version Management) を利⽤ • ⾃動化の設定 ◦ Slack で GitHub 通知の購読設定 ◦ テスト、リントチェック ◦ レビューアサイン Session0: Setup
  8. CI ② ↓が設定されたプロジェクトを自動生成 - CI - FVM - lint Session0:

    Setup レビュアーを⾃動アサインするワークフロー
  9. 天気予報アプリの画⾯レイアウトを構成する レイアウト仕様 • Placeholder の幅は画⾯の幅の半分 • ⻘字と⾚字の Text の幅は Placeholder

    の幅の半分 • Placeholder の⾼さと幅は同じ • Text のパディングや表⽰位置、スタイルの指定 • Placeholder と Text を合わせた矩形の中央は画⾯の中 央と同じ • TextButton の表⽰位置を指定 Session1: Layout
  10. 天気予報アプリの画⾯レイアウトを構成する レイアウト仕様 • Placeholder の幅は画⾯の幅の半分 • ⻘字と⾚字の Text の幅は Placeholder

    の幅の半分 • Placeholder の⾼さと幅は同じ • Text のパディングや表⽰位置、スタイルの指定 • Placeholder と Text を合わせた矩形の中央は画⾯の中 央と同じ • TextButton の表⽰位置を指定 Session1: Layout
  11. 陥りやすい実装の流れ 1. Placeholder を Center でラップ 2. Column の⼦ Widget

    に Placeholder と Text を配置 a. MainAxisAlignment.center Session1: Layout
  12. 陥りやすい実装の流れ 1. Placeholder を Center でラップ 2. Column の⼦ Widget

    に Placeholder と Text を配置 a. MainAxisAlignment.center 3. SizedBox(height: 80) SizedBox Session1: Layout
  13. 陥りやすい実装の流れ 1. Placeholder を Center でラップ 2. Column の⼦ Widget

    に Placeholder と Text を配置 a. MainAxisAlignment.center 3. SizedBox(height: 80) 4. TextButton を配置 Session1: Layout
  14. 陥りやすい実装の流れ 1. Placeholder を Center でラップ 2. Column の⼦ Widget

    に Placeholder と Text を配置 a. MainAxisAlignment.center 3. SizedBox(height: 80) 4. TextButton を配置 Session1: Layout
  15. レビュー観点 • 縦画⾯固定を適切に設定しているか • AspectRatio を使⽤しているか • MediaQuery の代わりに FractionallySizedBox

    を使⽤しているか • Expanded と Flexible を使い分けているか • 不必要に Container を利⽤していないか • Theme を適宜利⽤しているか • Column を余分に利⽤していないか Session1: Layout
  16. レビュー観点 • 縦画⾯固定を適切に設定しているか • AspectRatio を使⽤しているか • MediaQuery の代わりに FractionallySizedBox

    を使⽤しているか • Expanded と Flexible を使い分けているか • 不必要に Container を利⽤していないか • Theme を適宜利⽤しているか • Column を余分に利⽤していないか Session1: Layout
  17. レビュー観点 • pubspec.yaml に画像を追加する際、ディレクトリで指定しているか • YumemiWeather() を build 関数内でインスタンス化していないか •

    API の使⽤箇所でエラーハンドリングしているか • 天気の種類をenumで扱っているか • Enum.values.byName() を使⽤していないか • テストのことを考えて、YumemiWeather のインスタンスを DI しているか • アセットの指定間違い防⽌を考慮できているか Session2: API
  18. レビュー観点 • pubspec.yaml に画像を追加する際、ディレクトリで指定しているか • YumemiWeather() を build 関数内でインスタンス化していないか •

    API の使⽤箇所でエラーハンドリングしているか • 天気の種類をenumで扱っているか • Enum.values.byName() を使⽤していないか • テストのことを考えて、YumemiWeather のインスタンスを DI しているか • アセットの指定間違い防⽌を考慮できているか Session2: API
  19. レビュー観点 • pubspec.yaml に画像を追加する際、ディレクトリで指定しているか • YumemiWeather() を build 関数内でインスタンス化していないか •

    API の使⽤箇所でエラーハンドリングしているか • 天気の種類をenumで扱っているか • Enum.values.byName() を使⽤していないか • テストのことを考えて、YumemiWeather のインスタンスを DI しているか • アセットの指定間違い防⽌を考慮できているか Session2: API
  20. レビュー観点 • pubspec.yaml に画像を追加する際、ディレクトリで指定しているか • YumemiWeather() を build 関数内でインスタンス化していないか •

    API の使⽤箇所でエラーハンドリングしているか • WeatherCondition の enum を作成しているか • Enum.values.byName() を使⽤していないか • テストのことを考えて、YumemiWeather のインスタンスを DI しているか • アセットの指定間違い防⽌を考慮できているか Session2: API
  21. StatefulWidget のライフサイクル 課題詳細 • StatefulWidget で構築された新しい画⾯を追加する • 新しい画⾯の背景⾊は Colors.green に設定する

    • アプリ起動時に新しい画⾯に遷移する • 新しい画⾯が表⽰されたら、0.5秒後に天気の画⾯に遷 移する • 前回まで作っていた画⾯の Close ボタンをタップすると 新しい画⾯に戻る Session3: Lifecycle
  22. StatefulWidget のライフサイクル 課題詳細 • StatefulWidget で構築された新しい画⾯を追加する • 新しい画⾯の背景⾊は Colors.green に設定する

    • アプリ起動時に新しい画⾯に遷移する • 新しい画⾯が表⽰されたら、0.5秒後に天気の画⾯に遷 移する • 前回まで作っていた画⾯の Close ボタンをタップすると 新しい画⾯に戻る Session3: Lifecycle
  23. State のライフサイクル ライフサイクルメソッド • StatefulWidget.createState • State ◦ initState ◦

    dispose ◦ setState ◦ build ◦ didChangeDependncies ◦ didUpdateWidget Session3: Lifecycle ライフサイクルの状態
  24. State のライフサイクル Session3: Lifecycle created initialized initState() • State オブジェクトが生成された状態

    • initState() が呼び出される • State オブジェクトが Widget tree に初めて 挿入される時に一度だけ呼び出される • initState() が呼び出された状態 • didChangeDependencies() が呼び出される • ビルドの準備はできていない
  25. レビュー観点 • build メソッド直下で画面遷移処理を書いていないか • sleep を使っていないか • mounted のチェックをしているか

    • 初期画面と天気画面の Widget を分割しているか • iOS・Android のスワイプバックを考慮しているか Session3: Lifecycle
  26. レビュー観点 • build メソッド直下で画面遷移処理を書いていないか • sleep を使っていないか • mounted のチェックをしているか

    • 初期画面と天気画面の Widget を分割しているか • iOS・Android のスワイプバックを考慮しているか Session3: Lifecycle
  27. Mixin パターン • AutomaticKeepAliveClientMixin[5] ◦ AutomaticKeepAlive のクライアントのための便利なメソッドを持つ Mixin ◦ Stateサブクラスで使⽤する

    • TextSelectionDelegate[6] ◦ ツールバーやショートカットキーで選択範囲を操作するための Mixin • after_layout[7] ◦ Widget の最初のレイアウトが実⾏された後、つまり最初の frame が表⽰された 後にコードを実⾏する機能をもたらす Mixin Session4: Mixin
  28. Session4 課題 • Session3 で作成した ↓ の処理を after_layout のような Mixin

    を使って書き直す 新しい画⾯が表⽰されたら、0.5 秒後に前回まで作っていた画⾯に遷移する Session4: Mixin
  29. API のエラーハンドリング • yumemi_weather の API を捕捉してダイアログを表⽰ ◦ 使⽤する API

    をエラーを投げる API に置き換える ◦ API のエラーを補⾜して、エラーの内容に応じて AlertDialog でメッセージを表⽰する ◦ AlertDialog の OK ボタンをタップすると、ダイア ログを閉じる Session5: Error
  30. • ネットワークエラー • API レスポンスエラー • 認証エラー • リクエスト制限エラー •

    不明なエラー Session5: Error ⚠エラーが 発生しました OK エラーの内容によってメッセージを分けているか
  31. • ネットワークエラー • API レスポンスエラー • 認証エラー • リクエスト制限エラー •

    不明なエラー Session5: Error ⚠エラーが 発生しました エラーの内容によってメッセージを分けているか ユーザ体験が低下 する可能性が⾼い OK
  32. • ネットワークエラー ‧‧‧ ネットワークに接続されていません • API レスポンスエラー ‧‧‧ サービスに⼀時的な問題が発⽣しています •

    認証エラー ‧‧‧‧‧‧‧‧ ユーザ名またはパスワードが正しくありません • リクエスト制限エラー ‧‧‧ 要求数の制限に達しました • 不明なエラー ‧‧‧‧‧‧‧不明なエラーが発⽣しました Session5: Error エラーの内容によってメッセージを分けているか
  33. エラーハンドリングが公式ドキュメント[8]に従っているか • on 句を使⽤せずの catch は避けるべきである ( AVOID catches without

    on clauses ) • on 句なしの catch で捕捉したエラーを捨ててはならない ( DON’T discard errors from catches without on clauses ) • プログラム上のエラーについてのみ、Error を実装したオブジェクトを投げるべきであ る ( DO throw objects that implement Error only for programmatic errors ) • Error またはそれを実装した型を明⽰的に catch することは避けるべきである ( DON’T explicitly catch Error or types that implement it ) • 捕捉した例外を再スローする際には rethrow を使⽤するべきである ( DO use rethrow to rethrow a caught exception ) Session5: Error
  34. Session6 課題 • 使用する API を fetchThrowsWeather() から fetchWeather() に変更する

    • API から受け取った天気状況・最低気温・最高気温を画面に表示する Session6: Json
  35. レビュー観点 • build.yaml の設定が正しくできているか ◦ コード⽣成 ファイルの lint 対応ができているか ◦

    デフォルト値のものを記載しない ◦ checked: true にしているか ◦ field_rename: snake をつけているか Session7: Serialization
  36. レビュー観点 • build.yaml の設定が正しくできているか ◦ コード⽣成 ファイルの lint 対応ができているか ◦

    デフォルト値のものを記載しない ◦ checked: true にしているか ◦ field_rename: snake をつけているか Session7: Serialization
  37. レビュー観点(Riverpod) • build 関数直下で ref.read していないか • クリックイベントで ref.watch を使っていないか

    • .autoDispose を適切に設定しているか • Provider の依存関係図を表⽰している場合、依存関係図を⾃動⽣成できるようにして いるか Session8: StateManagement
  38. レビュー観点(Riverpod) • build 関数直下で ref.read していないか • クリックイベントで ref.watch を使っていないか

    • .autoDispose を適切に設定しているか • Provider の依存関係図を表⽰している場合、依存関係図を⾃動⽣成できるようにして いるか(Good/FYI) Session8: StateManagement
  39. Unit tests を書く Flutter のテストは次の3つに分類される • Unit tests • Widget

    tests • Integration tests Unit tests は1つの関数、メソッド、クラスの動作を確認するのに便利 また、他のテストより依存関係を少なくすることができるため、実装やメンテナンスコスト を低く抑えることができる Session9: UnitTest
  40. Session9 課題 • yumemi_weather API の呼び出しから Widget へ通知する部分までの Unit tests

    を書く ◦ 依存しているものが、成功‧失敗するケースも網羅する • 余⼒があれば、JSON のエンコード‧デコードの Unit tests も書く ※ テストコードを書くにあたって、依存関係を⾒直すなどのリファクタリングを⾏っても問 題ない Session9: UnitTest
  41. レビュー観点 • テストしやすい構成にリファクタリングした際に riverpod_graph を更新しているか • @visibleForTesting を適宜利⽤できているか • 不必要な

    group を作成していないか • テスト実⾏後に ProviderContainer を dispose しているか • テストダブルを適切に使えているか • テストダブルを利⽤した関数を呼び出す際に any を適宜利⽤しているか • 失敗ケースのテストも書いているか • 例外の場合 thenThrow を使⽤しているか • テストの description が適切な表現になっているか Session9: UnitTest
  42. Widget test を書く Flutter のテストは次の3つに分類される • Unit tests • Widget

    tests • Integration tests Widget Test はUI が期待通りに表⽰され、動作することを確認するのに便利 Unit Test より依存が多くなり、Widget のライフサイクルを適切に再現するための専⽤のテ スト環境が必要 Session10: WidgetTest
  43. Session10 課題 • 次の Widget Test を書く ◦ 特定の条件で、天気予報画⾯に ▪

    晴れの画像が表⽰されること ▪ 曇りの画像が表⽰されること ▪ ⾬の画像が表⽰されること ▪ 最⾼気温が表⽰されること ▪ 最低気温が表⽰されること ▪ ダイアログが表⽰され、特定のメッセージが表⽰されること Session10: WidgetTest
  44. レビュー観点 • private メソッドはテストから参照できない • テストのために public に変更すると、外部から参照できてしまう Session10: WidgetTest

    @visibleForTesting を適宜利⽤できているか @visibleForTesting を付与すると test フォルダからのみの参照を許可
  45. isolate で扱うべき処理について • スレッドをブロックする処理 ◦ 画像、⾳声処理など、CPU 負荷が⾼い処理 ◦ ⼤規模な JSON

    データの変換処理 Session11: ThreadBlock 本セッションの JSON 変換処理は重たくないので やらなくてもOK
  46. 参考 Resources: [1] https://github.com/yumemi-inc/flutter-training-template [2] https://notion.yumemi.co.jp/flutter%E7%A0%94%E4%BF%AE%E8%AA%B2 %E9%A1%8C%E3%81%AE%E3%83%AC%E3%83%93%E3%83%A5%E3%83 %BC%E8%A6%B3%E7%82%B9%E8%A1%A8 [3] https://fvm.app/

    [4] https://yumemi-inc.github.io/flutter-training-template/ [5] https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixi n.html [6] https://api.flutter.dev/flutter/services/TextSelectionDelegate-mixin.html [7] https://pub.dev/packages/after_layout [8] https://dart.dev/effective-dart/usage#error-handling [9] https://pub.dev/packages/json_serializable [10] https://pub.dev/packages/freezed [11] https://pub.dev/packages/flutter_riverpod [12] https://github.com/rrousselGit/riverpod/tree/master/packages/riverpod_graph [13] https://pub.dev/packages/mockito [14] https://medium.com/flutter-jp/isolate-a3f6eab488b5 参考 Attributions: • https://github.com/logos • https://www.flaticon.com/free-icon/communications_6134704 • https://www.flaticon.com/free-icon/svg_337954 • https://www.flaticon.com/free-icon/folder_716784 • https://www.flaticon.com/free-icon/cloudy_1163661 • https://www.flaticon.com/free-icon/smartphone_7344131 • https://www.flaticon.com/free-icon/folder_11580838 • https://www.flaticon.com/free-icon/database_1602309 • https://www.flaticon.com/free-icon/merge_7382043 • https://www.flaticon.com/free-icon/documents_3073439 • https://www.flaticon.com/free-icon/file_342348 • https://www.flaticon.com/free-icon/diagram_2500189 • https://www.flaticon.com/free-icon/flexibility_9583638