弊社Android担当のd-ariakeです。
この度、弊社もFlutterを採用することにしました。
ここ1週間ほど前からFlutterを入門していまして、最低限の動くものが作れるようになってきたので、ざっくりと学習メモのような感じで知見を残しておこうと思います。
サンプルアプリについて
Flutterの学習を進めつつ、シンプルなアプリを作ってみました。
リポジトリとアプリの動作イメージは以下の通りとなります。
Qiitaの記事検索APIを使用し、技術記事をキーワードで検索してリストで表示するといった動作となります。
(このイメージでは1つ記事しか表示されていませんが、実際は検索キーワードに応じた記事がちゃんと表示されるようになっています。)
ポイント
Flutterの入門をする上でポイントであると思ったのは以下の点です。
- BLoCパターン
- 静的解析
- CI
これらについて軽く知見を共有していきます。
BLoCパターン
BLoC (Business Logic Component) パターンはFlutterでよく使われる設計パターンです。
実装をする際には以下のルールに従います。
- Inputs and outputs are simple Streams/Sinks only
- Dependencies must be injectable and platform agnostic
- No platform branching allowed
- Implementation can be whatever you want if you follow the previous rules
引用元: Flutter / AngularDart – Code sharing, better together (DartConf 2018)
これらのルールに従って実装したBLoCは以下のようになります。
class SearchPageBloc { SearchPageBloc(ArticleRepository repository) : this._repository = repository { this._searchEventSubject.listen((_) => this._search()); } final ArticleRepository _repository; final _articleListSubject = BehaviorSubject.seeded(List<Article>.empty()); final _isFetchingSubject = BehaviorSubject.seeded(false); final _keywordSubject = BehaviorSubject.seeded(''); final _searchEventSubject = PublishSubject<void>(); // 記事リストを通知するStream Stream<List<Article>> get articleList => this._articleListSubject.stream; // 取得中かどうかを通知するStream Stream<bool> get isFetching => this._isFetchingSubject.stream; // 検索キーワードを流すSink Sink<String> get keywordSink => this._keywordSubject.sink; // 検索ボタンが有効かどうかを通知するStream Stream<bool> get isSearchButtonEnabled => Rx.combineLatest2( this._keywordSubject, this.isFetching, (String keyword, bool isFetching) => keyword.isNotEmpty && !isFetching, ); // 検索が要求されたことを流すSink Sink<void> get searchEvent => this._searchEventSubject.sink; void dispose() { /* 省略 */ } // 検索を行う。 Future<void> _search() async { /* 省略 */ } }
ルール 1. の通りに、BLoCからWidgetへの変更通知を行うもの (表示データや活性フラグなど) は Stream<T>
で公開し、WidgetからBloCへの通知を行うもの (入力イベントなど) は Sink<T>
で公開しています。
また、BLoC内部でのストリームの変換には RxDart を利用しています。
例えば以下の部分です。
// 検索ボタンが有効かどうかを通知するStream Stream<bool> get isSearchButtonEnabled => Rx.combineLatest2( this._keywordSubject, this.isFetching, (String keyword, bool isFetching) => keyword.isNotEmpty && !isFetching, );
Rxの combineLatest2()
を使い、
- 検索キーワードのストリーム (
_keywordSubject
) - データの取得中フラグのストリーム (
isFetching
)
の2つのストリームから検索ボタンの活性状態に変換するようなストリーム (isSearchButtonEnabled
) を作っています。
また、ルール 2. の通り、このBLoCは依存しているリポジトリ (ArticleRepository
) を constructor injection で注入可能な仕組みになっています。
もしテストをしたければ、コンストラクタに Mockito などで作ったモックを渡してやることでかんたんに通信部分の挙動を変えることができます。
そして、このBLoCをWidgetに持たせる部分ですが、私は provider を使って実装しました。
// 検索ページ class SearchPage extends StatelessWidget { @override Widget build(BuildContext context) => Provider<SearchPageBloc>( create: (context) => SearchPageBloc(Dependency.resolve()), dispose: (context, bloc) => bloc.dispose(), child: _SearchPageContent(), ); }
create
でBLoCのインスタンスを生成する処理を書きます。
コンストラクタに引数が必要なので Dependency.resolve<T>()
という get_it の ラッパ を介して、リポジトリのインスタンスをサービスロケートしています。
(単体テストはドメイン層からBLoCやViewModelまでの部分に対して書くのが最も費用対効果が高いと思っているので、今のところ provider#create
でのサービスロケータで問題はないと思っています。UIの自動テストをやろうと思った場合は別のアプローチが必要になるかもしれません。)
Stream<T>
の変更に応じて、Widgetをリビルドする部分は StreamBuilder<T>
を使います。
監視対象の Stream<T>
と、その流れてきた値からWidgetをビルドする関数を渡してやります。
以下はプログレスインジケータを構築する部分のコードとなります。
// プログレスインジケータを構築する。 Widget _buildProgressIndicator(SearchPageBloc bloc) => StreamBuilder<bool>( initialData: false, stream: bloc.isFetching, builder: (context, snapshot) => snapshot.data ? const LinearProgressIndicator() : Container(), );
SearchPageBloc#isFetching: Stream<bool>
を監視して、取得中ならば LinearProgressIndicator
を返し、取得中ではなければ空っぽの Container
を返すことで、プログレスインジケータの表示・非表示を切り替えています。
(Androidでは visibility = View.INVISIBLE
というようにプロパティで制御をしていましたが、Flutterではまるごと再構築を行うことで制御するんですね。UIパーツが状態を持たないという考え方はちょっと新鮮です。)
というのが、BLoCパターンによる設計のポイントです。
他にもReduxやFluxなどもありますが、ひとまず標準的だと思われるBLoCパターンで実装を行っていました。
必要ならばこれらのアーキテクチャについても学んでおこうと思います。
静的解析
Dart & Flutterには標準で強力なフォーマッタ・Linterがついています。
analysis_options.yaml
という名前で設定ファイルを置いておくと、その設定に応じた静的解析を行ってくれます。
このサンプルアプリでも設定をしています。
https://github.com/BetaComputing/FlutterQiitaClient/blob/master/analysis_options.yaml
Dartのフォーマッタを走らせるには以下のコマンドを実行し、
dart format --fix
FlutterのLinterを走らせるには以下のコマンドを実行します。
flutter analyze
私は結構コードのスタイルを気にするタイプなのですが、これらを走らせることで誰か書いても良いコードスタイルになります。
そして、Dangerと組み合わせてCIで実行させることで、本質的でないコードレビューを避けることもできてとても良いですね。
みなさんもぜひ使いましょう!
CI
弊社では普段から GitHub を使っているので GitHub Actions でのCIも設定してみました。
設定ファイルは以下のような感じです。
name: CI # (※一部省略しています。) build: runs-on: ubuntu-latest steps: - name: Setup Flutter uses: subosito/flutter-action@v1 with: channel: 'stable' flutter-version: '1.22.2' - name: Restore dotenv run: echo ${{ secrets.DOT_ENV }} | base64 -d > .env - name: Restore dependencies run: flutter pub get - name: Build (Android) run: flutter build apk --debug - name: Test run: flutter test - name: Format and Report run: dart format --fix ./ > dart_format_report.txt - name: Analyze and Report continue-on-error: true run: flutter analyze > flutter_analyze_report.txt - name: Run Danger uses: danger/danger-js@9.1.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
flutter-action という便利なactionが公開されていたので、これを利用させてもらいました。
全体の処理の流れは以下のようになります。
- Flutterのセットアップ
- 環境変数 (flutter_dotenv) の復元
- 依存関係の復元
- ビルド (Android)
- 単体テスト
- フォーマット
- Lint
- Danger
フォーマッタとLinterを走らせて、その実行結果を Danger JS で報告させています。
(少し雑ですが dangerfile.ts も自分で書いてみました。)
もし、フォーマッタとLintで引っかかると以下のような怒られが生じてCIがコケます。
これで本質的なコードレビューに集中できると思います。
このように、開発ツール・CIまわりについても基本的な使い方を学ぶことはできたかと思います。
入門してみた感想
1週間程度の駆け足で入門してみましたが、Flutterはそこそこ使えると感じました。
Dartの機能不足感を感じたり、一部だけ実現が難しいUIがあったりしましたが、基本的なマテリアルデザインのアプリならば十分だと思います。
開発ツールも公式が必要なものを提供してくれていますし、ホットリロードも爆速で開発体験も非常に良かったです。
ということで、Flutterでの開発もバリバリ対応できますので、ネイティブ (Kotlin・Swift)・クロスプラットフォーム (Flutter・Xamarin.Forms) 問わず、ぜひ我々にお任せください!
お仕事お待ちしております!