この記事は Flutter Advent Calendar(カレンダー2)の1日目の記事です。
さて、2021年のアドベントカレンダーもスタートしまして、Flutterのカレンダーはカレンダー3つ分、記事の数にして__60以上__にもなっています(!)
これだけの数の記事がまとまって投稿されるのはとてもよい勉強になる一方で、情報量が多すぎて__ひとつひとつの記事を個別に理解するのが大変__、または前提知識が足りずに__記事の内容が頭に入ってこない__という場合も少なくないのではないかと思います。
そんな問題を解消すべく、この記事では__Flutterという技術を俯瞰しながら個別の記事が何を話題にしているのか、その背景にはどのような事情があるのかを把握できる__ようになることを目指して、Flutterに関するあれこれをかいつまんで説明してみたいと思います。
今日から投稿されるたくさんの記事を効果的に活用する手助けになればと思います。
本文
Flutterとは
まずはそもそもFlutterとはどのような技術なのかについておさらいしましょう。
flutter.devによると、Flutterは一言で以下のように説明されています。
Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase
Flutterを使うことで、__単一のコードベースでモバイル、Web、デスクトップなど複数のプラットフォーム向けのアプリケーションをビルド・デプロイできる__こと、またそれを達成するための__開発プロセスをがらっと変えるもの__であることがウリになっています。
実際にFlutterでちょっとしたアプリを開発してみれば、
- 使い慣れた__任意のエディタやIDE__で開発できる
- __ホットリロード・ホットリスタート__で動作確認がサクサクできる
- 実行時に選択するプラットフォームを切り替えるだけで__複数のプラットフォームで動作確認__できる
といった特徴によってこの「ウリ」が実現していて、従来のSwift/Kotlinといったいわゆる「ネイティブ」の開発とはまた違った感覚で開発できることが実感できるのではないかと思います。1
プラットフォームの対応については、Flutterが最初にstableとしてリリースされた2018年末時点ではAndroid/iOSといったモバイルが、2021年の3月のバージョン2ではWebが正式にサポートされ、現在はデスクトップの対応が進められている状況です。2
特にWebの対応については正式にサポートされたFlutter2以降、多くの開発者がFlutterでのWebアプリケーション開発に挑戦し、成果物を公開したりFlutterが対応しきれていない点、Flutterが適していない要件などを知見として発信しています。
このアドベントカレンダーでも多くの記事がWebの話題を挙げているように思います。
Flutterが複数のプラットフォームで実行可能であるとは言っても、ひとつひとつのプラットフォームには__アプリに対する機能的な制限や設計思想、細かな挙動の違いなど__の点で埋められない差分が数多く存在します。
iOSやAndroidといった「モバイル」だけでなく、Webブラウザやデスクトップといった全く違う概念で成り立つプラットフォームで動かすアプリをひとつのコードベースで開発しようとなると、それなりに苦労や問題が発生することも事実です。
そのあたりの試行錯誤や知見はこのアドベントカレンダーをはじめいろいろな場所で記事が公開されていますので、Flutterが万能ではないことを頭に置いた上で現実的にどのようなプラットフォームでどのような問題が発生し得るのかを把握していくとよいでしょう。
Everything is a widget
FlutterではUIの構築に Widget を利用します。
Widgetはテキストやボタンといった個別のUIパーツだけでなく、そのパーツの配置方法や領域の確保、アニメーションなども担当します。さらにそれだけではなく、画面遷移やアプリ全体のテーマの指定、多言語対応などを担当するのもやはりWidgetです。
このことをFlutterでは "Everything is a widget" という言葉で表現しています。そのため、__「Flutterを学ぶ」という行為の大部分は「Widgetを学ぶ」ことである__と言っても過言ではありません。
FlutterにはとにかくたくさんのWidgetが用意されています。それに加え、世界中の開発者が特定の用途で活用できるさまざまなWidgetをパッケージとして公開しています。
そのため、どれだけ多くのWidgetを知っているか、どれだけ多くのWidgetが使えるか、というのはFlutterにおけるアプリ開発効率に直結します。
このことはFlutterの公式ドキュメントの作りとしても意識されていて、たとえば以下のページでは代表的なWidgetの一覧が「カタログ」として公開されています。
また、まとまった量のリファレンスを腰を据えて読むのではなく、「日常的に」少しずつWidgetの引き出しを増やすための情報源として、Youtubeには Widget of the Week や Package of the Week といった動画シリーズが定期的に投稿されています。
必要なときに必要なWidgetを選択するためには、必要になったときにググるだけでなく日常的な情報収集によって「引き出しを増やす」習慣が重要になります。
Widgetの中には実際に使ってみることでようやく活用方法や使い方の感覚が身に付くものもありますので、他の開発者が実際に試して得られたひとつひとつのWidgetに対する知見を記事から収集することで、いざ自分がそのWidgetを必要になったときにスムーズにそれを導入できるようになります。
コンストラクタに渡す値の名前や仕様など細かい使い方は実際に使うときに改めて調べればよいことですので、まずは
- どんなWidgetが用意されているのか
- どんな時に使えるのか
- どんな落とし穴があるのか
というあたりをざっくり把握することを意識しながら「引き出し」を増やす感覚で読むとよいでしょう。
状態管理
Widgetを使って構築したUIをユーザーの操作などに反応して「動きのある」アプリにするためには「状態管理」という手法を理解する必要があります。
Flutterは「宣言的」にUIを構築するフレームワークです。
従来の「命令的」なUIの構築では
- ボタンがタップされたら
- すでに生成済みのテキストを表すViewオブジェクトを取得して
- オブジェクトが保持するテキストデータを変更する
という考え方でユーザーの操作をUIに反映させますが、「宣言的」なUI構築手法では
- ボタンがタップされたら
- 画面に表示すべきテキストデータ(状態)を修正し
- 画面を構築するためのWidgetを一度全て破棄して再生成(リビルド)する
という考え方をします。
これを、Flutterの公式ドキュメントでは UI=f(State)
という数式で表現しています。
引用元
https://docs.flutter.dev/development/data-and-backend/state-mgmt/declarative
この考え方に則ってコーディングすることで、 「どのような手順を踏んだ結果であれ、この状態の時UIは必ずこうなる」という状態が保証 されます。これはユーザーの操作によってUIに変化を与える要素が多ければ多いほど大きなメリットになります。
さて、そのような「状態」を保持するためのWidgetとして、Flutterフレームワークでは StatefulWidget
と InheritedWidget
という2つのWidgetが用意されています。
そもそもFlutterにおいて__Widgetは高速に破棄と再生成が繰り返されることを前提とした不変(immutable)なオブジェクトである__ため、可変な「状態」をWidget自身が保持することはできません。
そこで、 StatefulWidget
では状態を保持するためのオブジェクトとして State
をペアとして生成します。これにより、Widget単独で利用する「状態」を効率的に管理できるようになっています。
しかし、ある「状態」を利用したいのが必ずしも単独のWidgetとは限りません。多くの場合、複数のWidget(複数ページや複数パーツ)で同じ状態データを共有する必要性が出てきます。
そこで、子孫の(複数の)Widgetに対して共通の「状態」を提供するのが InheritedWidget
というもうひとつのWidgetです。これを使うことで、共通の「状態」を複数のページ、複数のWidgetで共有して更新したり参照したりできるようになっています。
ただし、ここでさらに問題になるのが InheritedWidget をそのまま使うとボイラープレート的な記述が多くなり、コードが冗長になってしまう という点です。
そこで、 InheritedWidget
の代替となるパッケージとして、 bloc
や provider
、 riverpod
といったさまざまな「状態管理パッケージ」がさまざまな開発者によって開発・公開されてきました。
これらのパッケージは状態管理やそのテストのための使いやすい仕組みを提供してくれる便利なものです。しかし一方で、使い方を理解するためには__パッケージごとの思想や設計、それに基づく挙動をしっかりと理解しなければならないという学習コストの問題__が発生します。
特に provider
や riverpod
は最近のFlutterアプリ開発では定番と言えるほど多くのプロジェクトで利用されるパッケージで、その利用方法やそれを活用したアプリ全体の設計、それによって生じるさまざまな問題やエラーへの対処法など、多くの記事が公開されています。
状態管理は、他にもそれはもう数多くのパッケージが次々と開発・公開されていて、Flutterアプリ開発を「難しい」と感じさせる要因にもなっています。
とはいえどのパッケージが優れていてどんな場合にも最適ということはなく、目の前の開発したいアプリの要件に合わせて適切なものを選択する必要があることを考えると、流行りのパッケージばかりを追うのではなく、視野を広くさまざまなパッケージの考え方や使い所を他のパッケージとの比較という観点で理解しておくことも非常に大切です。
さまざまな視点からの記事が数多く投稿されるアドベントカレンダーという機会を利用して、1つでも多くの状態管理パッケージをざっと頭に入れるように心がけるとよいでしょう。
アーキテクチャ
ある程度の規模のアプリを開発したいのであれば、チュートリアルアプリの延長ではなくアプリ全体のアーキテクチャをしっかりと考え、それに則って開発を進めることが、品質的、効率的な観点からも重要になります。
幸い、アプリ開発においてはFlutterが登場する前からMVVMやMVPといったいくつものパターンが考え出され、それをiOSやAndroidといったプラットフォーム、言語に当てはめる試みが昔から続けられ、その知見を元にFlutterアプリのアーキテクチャが考えられることが多いです。
ただし、それらのアーキテクチャもただ取り入れればよいというわけではなく、Flutterの宣言的なUI構築の手法や、先述のさまざまな状態管理パッケージを利用することを前提にした上でどう適用するか、という点がよく議論されています。
「riverpod
+ state_notifier
+ freezed
を使ってMVVMパターンでアプリ開発」のような具体的なパッケージやパターンを紹介する記事はすでにいくつも投稿されていますが、読み手としてはそれを「そのまま取り入れるもの」として捉えるのではなく、あくまで__「数ある手法のひとつ」として捉え、他にはどのような手法が考えられ、それによるメリット・デメリットはどのようなものなのか、ということを考えながら記事を読み込む__姿勢が実際にプロダクトを開発する際に生きるのではないかと思います。
ビルドと環境設定
実際にプロダクトを開発する際、「本番用アプリ」と「検証用アプリ」でサーバーの向き先やアプリIDなどビルド設定を切り替えたい場合が少なからず発生します。
また、ビルドを個々人のPCで行うのではなく、Codemagic
や Bitrise
といったCI/CDツールを利用して__誰でも同じ設定・同じ環境で手軽に自動でテスト・ビルドできる__という状態を構築することは継続的な開発とリリースのために重要になってきます。
これを実現するための手法は、意外とどこか公式ドキュメントの1ページにまとまって紹介されているということはなく、またiOSやAndroidといったビルド対象のプラットフォームの仕組みにも依存する場合があるために昔から試行錯誤が続けられてきた経緯があります。
ビルドの選択肢によってなにを切り替えたいのか、またCI/CDツールはどれを利用するのか、などによって具体的な方法や注意ポイントは変わります。効率的な理解のためには、これらの__細かな使い方を細かく理解しようとするのではなく、ざっと読んでどのような選択肢にどのような特徴があるのか、どんな場合に活用できそうかをイメージしながら関連する記事を読む__とよいでしょう。
また可能であれば、個別のプラットフォームとしてこのようなビルド設定の切り替えのためにどのような仕組みが用意されているのか、それはFlutterのどのような仕組みを使えばFlutter経由で利用できるのか、といった仕組みの部分を理解することを意識すれば、読んだ記事で紹介されていなかったパターンを実現したい場合にも応用を効かせられるのではないかと思います。
内部実装とElement
"Everything is a widget" の言葉の通り、Flutterにおいてアプリ開発者が主に扱うオブジェクトがWidgetです。
しかし、実際のUIの構築・描画処理はWidgetから生成された Element
や RenderObject
といった別のオブジェクトが担当する設計になっています。
Widgetはリビルドごとにオブジェクトの破棄と再生成が行われることは先述した通りですが、だからといってリビルドによってUIを描画するためのすべてのオブジェクトが作り替えられ、画面全体が再描画されることはありません。Element
が必要最低限の変更箇所を特定し、また RenderObject
が前回のレイアウト計算結果を元に必要最低限の変更部分の再計算を行うような最適化処理が実装されているためです。
また、StatefulWidget
や InheritedWidget
、またそれをベースとした状態管理パッケージの仕組みも、実体の大部分はこのElementであることがほとんどです。
公式ドキュメントにはこのような内部実装についても詳しく説明されており、中でも Inside Flutter はその内部実装を言葉で説明するとても有用なドキュメントです。
このあたりの内部的な話は「明日からすぐ使える」知識ではありませんが、じっくりと少しずつ理解を深めることで、ふいに発生する不具合の原因と適切な対処法を明確にしたり、個々のWidgetやパッケージの仕組みや使い方を理解するなど、FlutterのUIを構築していく上でのすべてのベースとなる知識となります。
開発の効率と品質を同時に高めることを目的に、__少しずつ理解できる部分から頭に入れ、実際にそれを検証するコードを書き、動作を確認しながら少しずつ理解を深めていく__とよいでしょう。
具体的な開発事例
Flutterは企業が開発するビジネスのためのアプリだけではなく、Firebase等のクラウド技術と組み合わせた個人開発とも相性の良いフレームワークです。
そのため、実際に「作ってみた」体験談を紹介する記事が数多く公開されています。
多くの場合、Flutterでアプリを開発して公開することを考えた場合、Flutterによるコーディングだけでなくアーキテクチャの検討やビルド環境の構築、そしてテストやストアへのリリース、さらにカスタマーサポートなど、「Flutter」という切り口だけでは得られない情報や知見が必要になってきます。そのような知見を得るとてもよい手段が先人たちの「作ってみた」記事です。
繰り返しですが、Flutterは個人開発ととても親和性の高いフレームワークです。そのため、他のプラットフォームに比べて実際に個人開発してリリースした知見は頻繁に記事として紹介されている印象を持っています。
Flutterを使ってどのようなアプリが開発できるのか(もしくはしやすい/しづらいのか)、Flutter技術以外にどのような知識が必要になるのか、公開のための手続きはどのような流れなのか、といった全体的なイメージを掴めれば、いざアプリのアイデアを実現させたくなった時の助けになるはずです。
まとめ
以上です。
テストやデザイン、セキュリティ、パッケージ開発など、まだまだ説明しきれていない切り口はたくさんありますが、よく技術記事として公開される切り口やその読み方をざっとまとめてみました。
先日開催された FlutterKaigi に引き続き、この1ヶ月間はFlutterに関する発信が一気に増えて情報収集が大変になる時期とは思いますが、そこから読み取るべきポイントを押さえて効率よくインプットすることも同時に意識してみるとその効果が何倍にもなるのではないかと思います。
宣伝
先述の FlutterKaigi に私も登壇し、 "Everything is an Element" というタイトルで30分ほど話させていただきました。
動画はアーカイブされていますので、よかったら他のトークも含めてぜひ見返してみてください。
また、「内部実装とElement」で触れた内容についてはさらに詳しくまとまった分量を書いた本を Zenn で販売中です。無料公開のチャプターだけでもちょっとした分量ですので、ぜひ読んでみてください。