はじめに
前回は、Flutterの概要と、その特徴的な開発手法について解説しました。第2回となる今回は、より実践的な内容としてFlutterプロジェクトの構造と、モバイルアプリ開発における重要課題である「状態管理」について深く掘り下げていきます。
本記事の主な目的は、Flutter開発の実務において直面する重要な設計上の決定について、その背景にある考え方を理解することです。従来のモバイルアプリ開発やWebフロントエンド開発の経験がある方々にとって特に有益な内容となるよう、他のフレームワークとの比較観点も交えながら解説を進めます。
なお、環境構築やプロジェクト作成の具体的な手順については、Flutterの公式ドキュメントで詳細に解説されているため、本記事ではそれらの基本的な手順の説明は最小限にとどめ、代わりにプロジェクト構造の意図や、状態管理の設計思想について、より多く解説していきます。
特に、状態管理についてはフロントエンド開発における永遠のテーマと言えるでしょう。単一の「ウィジェット(Widget:画面上の部品)」内での状態管理からアプリケーション全体でのグローバルな状態管理まで、適切な設計判断が求められます。Flutterは、この課題に対して独自のアプローチを提供しており、それがパフォーマンスの最適化にも直結しています。
それでは、Flutterプロジェクトの構造から見ていきましょう。プロジェクトの各構成要素が持つ役割と、それらが全体としてどのように連携するのか、実践的な観点から解説していきます。
Flutterプロジェクトの作成
Flutterでアプリケーション開発を始めるにあたって、開発環境の構築について簡単に紹介します。ここでは、開発に必要なツールのセットアップから、最初のプロジェクト作成までの手順を解説します。
開発環境の構築
Flutterの開発環境は、主に3つの要素から構成されています。Flutterフレームワーク本体、iOSアプリケーション開発用の「XCode」、そしてAndroidアプリケーション開発用の「Android Studio」です。これらは相互に連携して動作し、クロスプラットフォーム開発の基盤となります。
まず、Flutterフレームワークのインストールから始めます。Flutter公式サイトにアクセスし、ご利用のOSに対応した手順に従ってインストールを行ってください。
・XCode / Android Studio
次に、プラットフォーム固有の開発ツールをインストールします。これらのツールは実際のコーディングには使用しませんが、アプリケーションのビルドとテストでは不可欠です(もちろん、コーディングに利用しても構いません)。
・iOS開発環境(Mac環境のみ必要)
XCodeはMacのApp Storeから直接インストールできます。このツールは、iOSシミュレータの提供とiOSアプリケーションのビルドプロセスを担当します。Windows環境ではiOS向けの開発はできないことに注意してください。
・Android開発環境
Android Studioの公式サイトからご利用のOSに対応したファイルをダウンロードしてインストールします。このIDEは、Androidエミュレータの実行環境とビルドツールチェーンを提供します。
プロジェクトの新規作成
開発環境の準備が整ったら、次のコマンドでFlutterプロジェクトを作成します。
1 | flutter create sample_app |
このコマンドにおけるsample_app
はプロジェクトの識別子となります。ご自身で好きな名前に変更いただいて構いません。なお、命名規則として英小文字とアンダースコアを使用したスネークケースが推奨されています。このコマンドを実行すると、Flutterが自動的に以下の処理を行います。
- プロジェクトの基本構造の生成
- 必要な設定ファイルの作成
- プラットフォーム固有の初期設定
- 基本的なサンプルコードの配置
アプリの起動
プロジェクトが作成できたら、以下の手順に従ってアプリケーションを実行します。
- iOS SimulatorまたはAndroidエミュレータを起動
- プロジェクトディレクトリに移動し、以下のコマンドを実行
初回の実行時は、必要なコンポーネントのダウンロードと設定に時間がかかる場合があります。これは、開発環境の初期化処理が行われているためです。
以上の手順で基本的な開発環境のセットアップが完了し、Flutterアプリケーションの開発を開始できる状態となります。ここでは、作成されたプロジェクトの構造と、各ファイルの役割について詳しく解説していきます。
プロジェクトの中身
作成したばかりのFlutterプロジェクトは、以下のような構成になっています。
- android
- build
- ios
- lib
- linux
- macos
- test
- web
- windows
...
たくさんのディレクトリがありますが、このうちandroid
、ios
、linux
、macos
、web
、windows
には各プラットフォームに関連するファイルが配置されます。もしモバイルアプリの開発だけを行い、デスクトップ向けには開発しないのであれば、不要なプラットフォーム関連ファイルはディレクトリごと削除しても構いません。
build
前回でも紹介した通り、開発効率を向上させるため、DartとFlutterは「Just-In-Time(JIT: 実行時コンパイル)」と「Ahead-Of-Time(AOT: 事前コンパイル)」の両方のコンパイル方式をサポートしています。
buildディレクトリには、アプリケーションをコンパイルする際に生成されるファイル群が配置されます。
lib
libフォルダはFlutterアプリケーションの心臓部です。ここにすべてのDartコードを配置します。まだ作成したばかりのこのプロジェクトにはmain.dart
ファイルのみが存在しています。このファイルがアプリケーションのエントリーポイントとなります。
Flutterのコードは、このlib
配下にどんどん記述していきます。実装が進むにつれて、このディレクトリには様々なサブディレクトリやファイルが増えていくことでしょう。
pubspec.yaml
pubspec.yaml
にアプリケーションの設定を保持します。この設定ファイルは「YAML Ain't Markup Language(YAML)」と呼ばれるマークアップ言語を使用しています。これは、例えばReactではpackage.json
、RubyではGemfile
に相当する役割を果たします。具体的には、アプリケーションの名前、バージョン番号、依存関係、アセットを宣言します。
pubspec.lockはpubspec.yamlファイルの内容に基づいて生成されるファイルです。Gitリポジトリに追加することはできますが、手動で編集すべきではありません。
test
最後に、testフォルダがあります。ここには、ユニットテストとウィジェットテストを配置できます。
Widgetと状態管理
Flutterアプリを作成する準備が整いました。ここからは、Flutterアプリケーション開発において最も基本的な概念である「ウィジェット(Widget)」と「状態管理」について解説します。
Widgetの基本
ウィジェットの実装について、重要な要素を順を追って解説します。以下のコード例は、Flutterにおける最も基本的なウィジェットの実装パターンを示しています。1行ずつコードの意味を追っていきましょう。
1 | class MyWidget extends StatelessWidget { |
2 | const MyWidget({Key? key}) : super(key: key); |
5 | Widget build(BuildContext context) { |
6 | return Text('Hello, world!'); |
まず、クラスの定義においてStatelessWidget
を継承することで、このクラスが状態を持たないウィジェットであることを宣言します。この継承により、Flutterのウィジェットとして機能するために必要な基本的な機能が提供されます(StatelessWidget
については後述します)。
1 | class MyWidget extends StatelessWidget { |
コンストラクタの定義では、オプショナルなkey
パラメータを受け取ります。このkey
はFlutterのウィジェットツリー内でウィジェットを一意に識別するために使用されます(※ウィジェットツリーについては次回以降で詳しく解説します)。
1 | const MyWidget({Key? key}) : super(key: key); |
build
メソッドはStatelessWidget
クラスで定義された抽象メソッドをオーバーライドしたものです。build
メソッドから返されるウィジェットが実際に画面上に描画される要素となります。BuildContext
型のcontext
パラメータは、ウィジェットツリー内での位置情報やテーマなどの環境情報へのアクセスを提供します(※context
についても次回以降の連載で詳しく解説します)。
2 | Widget build(BuildContext context) { |
3 | return Text('Hello, world!'); |
buildメソッドから返されるWidgetが実際に画面上に描画される要素となります。この例では、Hello, world!
というテキストが画面に表示されることになります。
仮にReactで同様の実装をする場合は、以下のようになります。
1 | const MyComponent = () => { |
2 | return <div>Hello, world!</div>; |
Reactをはじめとするモダンなフロントエンドフレームワークに親しみのある方であれば、比較的容易に慣れるのではないでしょうか。
状態管理の基本
次に、状態管理の基本について紹介します。
アプリケーション開発の歴史において、状態管理の重要性は時代とともに増してきました。かつてのWebページは静的なコンテンツの表示が主でしたが、現代のアプリケーションは複雑な対話性を備え、動的なユーザーインターフェースが当たり前となっています。
「状態」とは、アプリケーションが保持する様々なデータを指します。ユーザーの入力値、サーバーから取得したデータ、アプリケーションの設定など、画面表示に影響を与えるすべての情報が状態に含まれます。この状態を適切に管理することは、アプリケーションの信頼性とユーザー体験に直接的な影響を与えます。
Flutterにおける状態管理は特に重要です。「Everything is a Widget」という設計思想に基づき、ユーザーインターフェースのすべての要素がWidgetとして表現されるためです。各Widgetは状態の変化に応じて適切に更新される必要があり、この更新プロセスを効率的に管理することが求められます。
Flutterのウィジェット設計
Flutterは、状態の有無によってウィジェットを大きく2つに分類します。「StatelessWidget(ステートレスウィジェット)」と「StatefulWidget(ステートフルウィジェット)」です。この明確な区分により開発者は状態管理の方針を意識的に選択できます。
StatelessWidget(状態を持たないウィジェット)
StatelessWidget
は一度作成されると内部の状態が変更されない、純粋に表示のための要素です。以下は典型的な実装例です。
1 | class MyWidget extends StatelessWidget { |
2 | const MyWidget({Key? key}) : super(key: key); |
5 | Widget build(BuildContext context) { |
StatelessWidget
は不変です。一度作成されると変更することはできません。つまり、Flutterフレームワークからすると一度だけ処理すれば良いのです。複雑なライフサイクルの状態を管理したり、変更を気にしたりする必要がありません。StatelessWidget
を変更する唯一の方法は、削除して新しいものを作成することです。
StatefulWidget(状態を持つウィジェット)
一方、StatefulWidget
は内部状態を持ち、その状態の変更に応じてユーザーインターフェースを更新します。実装は以下のように2つのクラス(以下の例ではMyHomePage
と_MyHomePageState
)で構成されます。
01 | class MyHomePage extends StatefulWidget { |
08 | _MyHomePageState createState() => _MyHomePageState(); |
11 | class _MyHomePageState extends State<MyHomePage> { |
15 | void incrementCounter() { |
22 | Widget build(BuildContext context) { |
27 | onPressed: incrementCounter, |
StatefulWidget
は少し複雑に見えますが、基本的な仕組みは以下のとおりです。
- 2つのクラスで構成される
StatefulWidget
クラス: ウィジェットの設定を定義State
クラス: 実際の状態と表示を管理
- クラスの役割
- 上部のクラス(例:
MyHomePage
)はStatefulWidget
を継承し、createState()
メソッドで対応するState
オブジェクトを作成します - 下部のクラス(例:
_MyHomePageState
)はState
を継承し、実際の状態(変数)と画面表示(build
メソッド)を管理します
- 状態の更新方法
- 状態(例:
counter
変数)を変更するときはsetState()
メソッドを使います setState()
を呼ぶとFlutterが自動的に画面を再描画してくれます
- 命名の慣習
StatefulWidget
の名前がHoge
なら、対応するState
クラスは_HogeState
と命名するのが一般的です- 先頭の
_
(アンダースコア)はDartでプライベート(非公開)を意味します
build
メソッドはStatelessWidget
と同様に、画面に表示する内容を定義するメソッドです。違いはStatefulWidget
では状態の変化に応じてbuild
メソッドが再実行され、画面が更新される点にあります。
Reactとの比較
Flutterの状態管理の概念は、Reactなどの他のUIフレームワークと多くの類似点があります。実装方法を比較することで、両者の設計思想の共通点と相違点を理解できます。
・StatelessWidgetとReactの純粋コンポーネント
FlutterのStatelessWidget
は、Reactの純粋関数コンポーネント(Pure Function Component)に相当します。
Flutter(StatelessWidget):
01 | class Greeting extends StatelessWidget { |
04 | const Greeting({Key? key, required this.name}) : super(key: key); |
07 | Widget build(BuildContext context) { |
08 | return Text('こんにちは、$name さん!'); |
React(関数コンポーネント):
1 | const Greeting = ({ name }) => { |
2 | return <div>こんにちは、{name} さん!</div>; |
どちらも外部から渡されたプロパティ(props)のみに依存し、内部状態を持ちません。入力が同じであれば常に同じ出力を返す「純粋」な性質を持っています。
StatefulWidgetとReactの状態管理
FlutterのStatefulWidget
はReactの状態を持つコンポーネントに相当します。Reactではクラスコンポーネントまたはフックを使用した関数コンポーネントで状態管理を実現します。
・カウンターアプリの例
Flutter (StatefulWidget):
01 | class Counter extends StatefulWidget { |
02 | const Counter({Key? key}) : super(key: key); |
05 | _CounterState createState() => _CounterState(); |
08 | class _CounterState extends State<Counter> { |
18 | Widget build(BuildContext context) { |
React (Hooks使用):
02 | const [count, setCount] = useState(0); |
04 | const increment = () => { |
11 | <button onClick={increment}>増加</button> |
状態更新メカニズムの類似点
両フレームワークの状態更新メカニズムには、以下のような類似点があります。
- 宣言型UI: 両者とも宣言型のアプローチを採用しており「何を表示するか」に焦点を当てています
- 状態更新による再描画:
- Flutter:
setState()
を呼び出すと、関連するState
オブジェクトが「dirty」としてマークされ、次のフレームでbuild
メソッドが再実行されます。 - React:
setState()
またはuseState
のセッター関数を呼ぶとコンポーネントの再レンダリングがスケジュールされます
- 単一方向データフロー: 両フレームワークともデータは親から子へと一方向に流れ、状態の変更は特定のメソッドを通じて行われます
ライフサイクルの概念
両フレームワークはコンポーネント/ウィジェットのライフサイクルという概念を持っています。
initState()
: コンポーネントの初期化(ReactのcomponentDidMount
やuseEffect([], ())
に相当)
01 | class CounterState extends State<Counter> { |
07 | // ウィジェットが初めて作成されたときに一度だけ実行される |
08 | _timer = Timer.periodic(Duration(seconds: 1), (timer) { |
dispose()
: コンポーネントの破棄(ReactのcomponentWillUnmount
に相当)
3 | // ウィジェットがウィジェットツリーから削除されるときに呼ばれる |
4 | _timer.cancel(); // リソースの解放 |
didUpdateWidget()
: プロパティ変更時(ReactのcomponentDidUpdate
に相当)
02 | void didUpdateWidget(Counter oldWidget) { |
03 | super.didUpdateWidget(oldWidget); |
04 | // 親ウィジェットが再構築され、このウィジェットの設定が変更されたときに呼ばれる |
05 | if (oldWidget.initialValue != widget.initialValue) { |
08 | count = widget.initialValue; |
相違点
主な相違点としては、FlutterはStatefulWidget
とState
を明確に分離する設計を採用している点があります。これによりウィジェットの設定と状態管理の責務が分離され、コードの構造化が促進されます。
これに対して、Reactでは特にフックの導入によりコンポーネントと状態管理がより統合されたアプローチとなっています(※ただし、Flutterにおいてもフックを利用するアプローチが浸透しつつあり、Reactのフック型APIに近い形で状態管理を行うことも可能になっています)。
効率的な再描画
Flutterの状態管理システムは効率的な再描画を実現するように設計されています。独自のレンダリングエンジンにより変更された部分のみを特定し、必要最小限の更新を行います。これは、多くの場合Reactなどのフレームワークでしばしば必要となる手動の最適化(メモ化など)を不要とします。
StatelessWidget
とStatefulWidget
の明確な区分は、この最適化プロセスを支援します。変わらない部品(StatelessWidget)と変わる部品(StatefulWidget)をはっきり分けることで「この部分は変わらないから、再計算しなくて良い」とFlutterが自動的に判断できるようになっています。
Reactのパフォーマンス最適化との比較
FlutterとReactでは、UIの更新処理に根本的な違いがあります。
- Flutter: 独自のレンダリングエンジンが変更された部分を直接GPUに描画します。ウィジェットツリーの差分を計算し、必要な部分だけを再構築します。これは「レイヤーツリー」と呼ばれる低レベルの描画命令に変換されます
- React: 仮想DOM(Virtual DOM)を使用し、実際のDOMとの差分を計算してから更新します。この2段階のプロセスは効率的ですが、DOMの操作自体が比較的重いため、Flutterの直接描画方式と比べると若干のオーバーヘッドが生じることがあります
Flutterのアプローチは、モバイルアプリケーションのような高パフォーマンスが求められる環境で特に効果的で、フレームワークは変更の追跡を効率的に行い、パフォーマンスを向上させることができます。
おわりに
今回は、実際にFlutterプロジェクトを作成し、その構造を解説しました。さらに、アプリケーション開発の要となる状態管理について学び、それがウィジェットとしてどのように表現されるかを詳しく見てきました。
次回は、この基礎的な理解を踏まえて、より実践的な内容に踏み込みながら、実例を交えつつ詳しく解説していく予定です。