FlutterKaigi 2023 をオンライン視聴した際のメモです。
出前館における Flutter の現在とこれから
概要
- React Native から Flutter で再実装&リリース(Rearchitect)した話
- 11/7 にリリースした
出前館のご紹介
- オンラインで密に連携している
- 韓国のチームとも連携している
- アプリエンジニア 20 人ほど
- 加盟店向けアプリ(タブレット):Xamarin → Flutter
- ドライバー向けアプリ:React Native → Flutter
- 出前館アプリ(エンドユーザー向け):React Native → Flutter
- 3、4 年かけて Flutter に置き換えてきた
出前館アプリ Rearchitect の取り組み
モチベーション
- 背景として、すでに他アプリで Flutter アプリを利用していたことがある
- 組織が利用する技術を統一し、アウトプットを最大化
- 状況に応じたアサインの柔軟さ
- 利用技術統一によるナレッジ共有の効率化
- Flutter への期待、新技術への好奇心
どう進めるか
- Type Script から Dart へ
- スケジュール調整
- 多くの新規案件
- 事業への影響
- Flutter の学習
- 多数の決済方法
機能要件
既存と同等の機能を提供する。
非機能要件
- コードフリーズ期間を設けることは難しい
- 案件調整、スケジュール管理など
- Flutter の開発も進めつつ、React Native での機能追加を並行する
- Flutter 版は韓国チームで先行して進め、徐々に日本チームも交流
- 勉強会、コードレビュー、お互いをリスペクトする姿勢
- 日本チームは Flutter をこれから学習する
- 勉強会、コードレビュー
- 不具合なく進めたい
- リグレッションテストの見直し、社内βテスト実施など
苦労したこと
- React Native におけるステート管理を Flutter らしいステート管理へ
- 読み替えに苦労した
- 両方の技術に精通することは容易でない
- Type Script から Dart への書き換え
- 言語特性の違いへの対応
- 韓国チームがすでに Flutter に慣れていたことが助けになった
リグレッションテストの見直し
- モチベーション
- テストケースへの不安(テスト観点の不足など)
- Rearchitect をしたプロダクトの品質保証
- 先行事例
- ソフトウェアが巨大になるにつれ、テストケースも増える
- すべてのテストを実施することが困難になる
- テストの効果を引き出すためのアプローチ
- Test Suite Minimization
- Test Case Selection
- Test Case Prioritization
- ソフトウェアが巨大になるにつれ、テストケースも増える
Test Analysis
ページ/機能単位のスコアリングを実施。
- Incident Probability
- 3 段階で評価
- 過去の障害、QA 時のバグ情報を参照
- 不具合報告書
- QA 時のバグチケット
- どのページ、機能で発生しやすいかをリスト化
- Complexity
- 3 段階で評価
- 状態の多寡でスコアリング
- Frequency of Use
- 3 段階で評価
- ユースケースを用いたスコアリング
Test Design、Test Implementation
- 成果物:UML Testing Profile, テストケース
- 進め方:優先度の高いテストの実施
- 3 か月ほど優先順位を見直しつつ実施
- 結果、バグ件数が順調に収束した
ゆめみの Flutter エンジニア育成方法
- Flutter の研修課題を用いて新人を教育している
- 天気予報アプリの開発を課題としている
- レビュー観点表を公開している
- ゆめみの採用の評価ポイントを知ることができる
- 「適切」という抽象的な表現を使う→プログラミングに絶対的な正解はないため
基礎的なレビュー観点
- Dart の基礎的な部分
- const を付与しているかどうかなど
- GitHub の基礎的な部分
- コミットの基本的な原則に沿っているかなど
- プログラミング の基礎的な部分
セッション 0 Set Up
- GitHub のリポジトリからテンプレートを作成する
- ワークフローから CI が実行されるようになっている
- FVM(Flutter のバージョンを管理するためのツール)を使用している
セッション 1 Layout
- 天気予報アプリの画面レイアウトを構成する
- 仕様を定義しておく
- レビュー観点
- AspectRatio を使用しているか
- MediaQuery の代わりに FractionallySizedBox を使用しているか→const 化できる
- 不用意に Container を利用していないか→const 化できないため
- etc
セッション 2 API
- 天気情報を返すための API(yumemi_weather_api)を実装する
- レビュー観点
- Enum.vallues.byName を使用していないか→コードの中でエラーをキャッチしないため
- etc
セッション 3 Lifecycle
- 画面遷移する
- State のライフサイクル
- created → initState() → initialized
- レビュー観点
- sleep を使っていないか→UI 処理をブロックしてしまうため
- mounted のチェックをしているか
- etc
セッション 4 Mixin
- Mixin パターン
- after_layout のような Mixin を使って遷移処理を書き直す
- レビュー観点
- Mixin で定義されているメソッドは abstract method になっているか
セッション 5 Error
- API がエラーを投げるよう書き換える
- レビュー観点
- エラー内容によってメッセージを分けているか
- エラーハンドリングが公式ドキュメント(Effective Dart)に沿っているか
セッション 6 JSON
- API から受け取ったデータ(JSON)を変換する
- あえてライブラリを使わずに自前で実装させることで、外部パッケージの便利さを認識させる
- レビュー観点
- すべての例外ケースを考慮して、JSON 変換処理が書けているか
- etc
セッション 7 Serialization
- 値のシリアライズ
- レビュー観点
- コード生成ファイルの Lint を無効化する
- etc
セッション 8 State Management
- 状態をどのように管理するか
- 様々な状態管理パッケージがある
- 研修では Riverpod を使用する
- レビュー観点
- テストがしやすい実装・構成になっているか
- Riverpod に関する観点もある
セッション 9 Unit Tests
- Unit Tests を書く
- Mockito などを使ってモック化して様々なテストケースに対応させる
- レビュー観点
セッション 10 ウィジェット Tests
- UI 周りのテストを書く(意図したダイアログが表示されるかなど)
- レビュー観点
- コンポーネントで完結するテストに画面遷移など他の要素が含まれていないか
-
@visibleForTesting
を適宜利用できているか- テストのために private メソッドを public に変えたくない
- etc
セッション 11 Thread Block
- Dart はシングルスレッド
- isolate を用いた並列処理を行う
- レビュー観点
- isolate で扱うべき処理について
- CPU 負荷が高い処理
- 大規模な JSON 変換処理
- isolate で扱うべき処理について
Flutter アプリのセキュリティ対策を考えてみる
- WINTICKET アプリの開発
- 競輪とオートレースのネット投票サービス
- Flutter 製
- セキュリティ考慮のきっかけになればよい、より高品質なアプリを
- セキュリティ対策を考えるためには攻撃手法を知ること
セキュリティ対策ってなに?
- セキュリティ対策はわからないことだらけ
- 何をしたら安全か
- そもそもどのような脅威があるか
- 比較して検討・対策することが重要
- 全ての攻撃に対して対策するのはほぼ不可能
- どこまで担保するかを決める
- Flutter のセキュリティ対策が公式ドキュメントにまとめられている
- Flutter の脆弱性を見つけたら、security@flutter.dev へ
- GitHub issue に挙げるのはリスクがあるのでダメ
- OWASP というコミュニティがある
- OWASP MASVSにまとめられている
- 最近のアプリについてリスクのトップ 10 についてリスト化して共有している
Flutter がモバイル端末で動く仕組み
- Flutter の FAQ に書かれている
- VM を使ってコードを実行
- 再コンパイルせずホットリロードをしている
- etc
- Dart VM
- Runtime System
- Just-in-Time:デバッグビルド時
- Ahead-of-Time:リリースビルド時
- TFA という静的解析が行われる
- CFE で Kernel AST という中間コードを生成する
- Kernel へのコンパイルと実行は完全分離している
Flutter アプリ開発者ができる対策
MASVS-STORAGE
- API キーなどをローカルに保存するケースがあるが、それらを保護できるようにする必要がある
- shared_preferences
- root 化するとデータを抜き取れてしまう
- flutter_secure_storage を使う
- WebView Cookie を適切なタイミングで削除しないと閲覧できてしまう
MASVS-NETWORK
- https は完全に安全ではない
- 正しい証明書を使っているかを判別するのが難しい
- 中間者攻撃のリスクがある
- 証明書と公開鍵を改竄する
- Certficate Pinning(SSL Pinning)を使う
- http_certificate_pinnging を使えば簡単に実現できる
- 運用上のデメリットとして、証明書には有効期限があるため、Finger Print の更新が必要
MASVS-PLATFORM
- モバイルアプリのセキュリティはモバイルプラットフォームとの相互作用に大きく依存している
- パスワードはアプリの UI に表示されることが多いが、表示時に情報漏洩するリスクがある
- アプリがバックグラウンドに移動した際、ウィジェット を切り替えることで回避できる
- secure_application を使えば簡単に実現できる
MASVS-RESILIENCE
- コードを難読化し、アンチデバッグ、改竄を防ぐ
- リバースエンジニアリングするときは reFlutter を使うと便利
- 実行中の Dart の関数の引数を除いたり、返り値を変更したりしたいときは Frida が便利
- flutter logs コマンドで実行中のアプリのログを覗ける
- libapp.so の専用の解析ツールがある
- 最新の Flutter バージョンでは使えない模様
- リバースエンジニアリングの対策
- Flutter Obfuscate を使って難読化する
- 注意点として、エラー監視ツールを使っている場合にエラー内容も難読化されてしまうのでエラー監視ツール側で対策する必要がある
まとめ
- 攻撃者のコスト・発生時の被害・対策コストを比較して検討する必要がある
- OWASP MAS が参考になる
- 攻撃の難易度を上げる対策をすることが大切
- Obfuscate は対応が簡単で、エラー監視ツールが対応していれば効果抜群
我々にはなぜ Riverpod が必要なのか - InheritedWidget から始まる app state 管理手法の課題
- 日本では 9 割以上のプロジェクトで採用されている
- なぜ採用したかの理由が説明できるか、採用基準を考える
- LivMap
- Provider から Riverpod へ移行中
Flutter における状態管理
宣言的な UI 構築とは
- build メソッドでウィジェットをどう配置するかを記載する
- ウィジェットツリーの構築
- build メソッドで「状態」を使う
- すでに生成済みのウィジェットは直接編集できない
- ウィジェットツリーの差分を検知して再計算と描画が行われる
Element と markNeedsBuild()
- Element
- ウィジェットと 1:1 で生成されるオブジェクト
- createElement() が呼び出されてウィジェットが Element を生成する
- Element は ウィジェットの参照を保持する
- 親子の参照を持っているのはウィジェットではなく Element
- BuildContext オブジェクトも実体は Element
- リビルド時、リビルド範囲内のウィジェットは基本的に全て再生成される
- Element は可能な限り再利用される
- RenderObject やレイアウト計算の結果なども可能な限り再利用される
- ウィジェットのリビルドと RenderObject の再描画は分けて考える
- markNeedsBuild()
- markNeedsBuild() が呼び出されると対応するウィジェットの build()が呼ばれてリビルドされる
- どのような管理手法であっても同じ
ephemeral state と app state
- ephemeral state:単一のウィジェット内でのみ参照される状態
- app state(shared state):複数のウィジェットから参照できる共通の状態
StatefulWidget と InheritedWidget
- StatefulWidget:ephemeral state を管理するためのウィジェット
- InheritedWidget:app state を管理するためのウィジェット
状態管理の課題と Riverpod の戦略
Riverpod とは
- InheritedWidget の再実装を目指している状態管理パッケージ
- app state の管理が主な役割
- StatefulWidget の代替ではない
- ウィジェットツリーは極力利用しない作りになっている
- 状態管理における「よくある課題」への解決策を幅広く用意してくれている
- 「状態」を構成する 3 つの要素
- Provider:state にアクセスするためのキー
- メンテナ(造語):state を生成、更新するもの
- state:状態そのもの
状態の生成、更新、破棄
生成
- 課題
- 状態の生成は極力遅延したい
- まだ使わない API のリクエストは避けたい
- etc
- 解決方法
- はじめて Ref を使ってアクセスしたときに
- Provider オブジェクトが生成される
- 必要に応じて Notifier オブジェクトが生成される
- 関数/build()メソッドによって state が生成される
- ref.read / ref.watch / ref.listen するまで生成されない
- はじめて Ref を使ってアクセスしたときに
更新
- 課題
- 状態の更新を安全に行いたい
- 意図しない箇所から変更させたくない
- 解決方法
- 状態の更新処理がメンテナの中で完結することを強制される
- メンテナ(Notifier)で用意した処理でしか状態を更新できない
- ref.watch を使う方法もある
更新の通知
- 課題
- 状態の変更が過不足なく利用箇所へ通知されてほしい
- Dart では値が変化したことを通知する手段がない
- 命令的な処理を実行したい場合もある
- 解決方法
-
state = newState
の書き方、もしくはリビルドによる変更を強制する - 状態が immutable であることを前提としている
- 関数のコールバックを待ち受ける ref.listen が用意されている
-
破棄
- 課題
- 不要になった状態は過不足なく破棄したい
- 任意のタイミングでも状態を破棄・再生成したい
- 解決方法
- autoDispose の仕組みを使って自動的に破棄する
- keepAlive オプションで破棄されない状態も生成できる
- ref.invalidate / ref.refresh により任意のタイミングで破棄できる
状態同士の連携
- 課題
- ある状態を元に、別の状態を「宣言的に」生成したい
- 元にした状態の変更を検知して、依存している状態も変更したい
- 解決方法
- 各メンテナが持つ Ref を利用して、他の状態にアクセスできる
- ref.watch を使うことでウィジェットと同じように「宣言的に」状態を更新できる
非同期な状態の更新
- 課題
- 状態の生成に非同期な処理が含まれる場合、状態の生成処理自体に状態が発生する
- 処理中、処理完了、処理中にエラー発生、リトライなど
- 2 回目があるかも
- 状態の生成に非同期な処理が含まれる場合、状態の生成処理自体に状態が発生する
- 解決方法
- AsyncValue クラスを使う
- AsyncLoading、AsyncData、AsyncError クラスがある
- Riverpod ではアプリにおけるほとんどの状態生成が非同期のはずという前提がある
- AsyncValue クラスを使う
テスト
- Provider の差し替えができるのでテストがしやすい
まとめ
- Riverpod を使うことで、一般的に発生する課題に対してあらかじめ用意された解決策を利用でき、アプリ独自に考える必要がなくなる
- あらかじめ用意された解決策自体への理解が必要