「Flutter」のプロジェクト構造と状態管理でアプリ開発を標準化する

2025年4月4日(金)
加納 愼之典
第2回の今回は、Flutterのプロジェクト構造と、Stateless/Statefulウィジェットによる状態管理の基本を解説します。

はじめに

前回は、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プロジェクトを作成します。

flutter create sample_app

このコマンドにおけるsample_appはプロジェクトの識別子となります。ご自身で好きな名前に変更いただいて構いません。なお、命名規則として英小文字とアンダースコアを使用したスネークケースが推奨されています。このコマンドを実行すると、Flutterが自動的に以下の処理を行います。

  • プロジェクトの基本構造の生成
  • 必要な設定ファイルの作成
  • プラットフォーム固有の初期設定
  • 基本的なサンプルコードの配置

アプリの起動

プロジェクトが作成できたら、以下の手順に従ってアプリケーションを実行します。

  • iOS SimulatorまたはAndroidエミュレータを起動
  • プロジェクトディレクトリに移動し、以下のコマンドを実行
flutter run

初回の実行時は、必要なコンポーネントのダウンロードと設定に時間がかかる場合があります。これは、開発環境の初期化処理が行われているためです。

以上の手順で基本的な開発環境のセットアップが完了し、Flutterアプリケーションの開発を開始できる状態となります。ここでは、作成されたプロジェクトの構造と、各ファイルの役割について詳しく解説していきます。

プロジェクトの中身

作成したばかりのFlutterプロジェクトは、以下のような構成になっています。

  • android
  • build
  • ios
  • lib
  • linux
  • macos
  • test
  • web
  • windows
    ...

たくさんのディレクトリがありますが、このうちandroidioslinuxmacoswebwindowsには各プラットフォームに関連するファイルが配置されます。もしモバイルアプリの開発だけを行い、デスクトップ向けには開発しないのであれば、不要なプラットフォーム関連ファイルはディレクトリごと削除しても構いません。

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行ずつコードの意味を追っていきましょう。

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text('Hello, world!');
  }
}

まず、クラスの定義においてStatelessWidgetを継承することで、このクラスが状態を持たないウィジェットであることを宣言します。この継承により、Flutterのウィジェットとして機能するために必要な基本的な機能が提供されます(StatelessWidgetについては後述します)。

class MyWidget extends StatelessWidget {

コンストラクタの定義では、オプショナルなkeyパラメータを受け取ります。このkeyはFlutterのウィジェットツリー内でウィジェットを一意に識別するために使用されます(※ウィジェットツリーについては次回以降で詳しく解説します)。

const MyWidget({Key? key}) : super(key: key);

buildメソッドはStatelessWidgetクラスで定義された抽象メソッドをオーバーライドしたものです。buildメソッドから返されるウィジェットが実際に画面上に描画される要素となります。BuildContext型のcontextパラメータは、ウィジェットツリー内での位置情報やテーマなどの環境情報へのアクセスを提供します(※contextについても次回以降の連載で詳しく解説します)。

@override
  Widget build(BuildContext context) {
    return Text('Hello, world!');
  }

buildメソッドから返されるWidgetが実際に画面上に描画される要素となります。この例では、Hello, world!というテキストが画面に表示されることになります。

仮にReactで同様の実装をする場合は、以下のようになります。

const MyComponent = () => {
  return <div>Hello, world!</div>;
};

Reactをはじめとするモダンなフロントエンドフレームワークに親しみのある方であれば、比較的容易に慣れるのではないでしょうか。

状態管理の基本

次に、状態管理の基本について紹介します。

アプリケーション開発の歴史において、状態管理の重要性は時代とともに増してきました。かつてのWebページは静的なコンテンツの表示が主でしたが、現代のアプリケーションは複雑な対話性を備え、動的なユーザーインターフェースが当たり前となっています。

「状態」とは、アプリケーションが保持する様々なデータを指します。ユーザーの入力値、サーバーから取得したデータ、アプリケーションの設定など、画面表示に影響を与えるすべての情報が状態に含まれます。この状態を適切に管理することは、アプリケーションの信頼性とユーザー体験に直接的な影響を与えます。

Flutterにおける状態管理は特に重要です。「Everything is a Widget」という設計思想に基づき、ユーザーインターフェースのすべての要素がWidgetとして表現されるためです。各Widgetは状態の変化に応じて適切に更新される必要があり、この更新プロセスを効率的に管理することが求められます。

Flutterのウィジェット設計

Flutterは、状態の有無によってウィジェットを大きく2つに分類します。「StatelessWidget(ステートレスウィジェット)」と「StatefulWidget(ステートフルウィジェット)」です。この明確な区分により開発者は状態管理の方針を意識的に選択できます。

StatelessWidget(状態を持たないウィジェット)

StatelessWidgetは一度作成されると内部の状態が変更されない、純粋に表示のための要素です。以下は典型的な実装例です。

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return HogeWidget();
  }
}

StatelessWidgetは不変です。一度作成されると変更することはできません。つまり、Flutterフレームワークからすると一度だけ処理すれば良いのです。複雑なライフサイクルの状態を管理したり、変更を気にしたりする必要がありません。StatelessWidgetを変更する唯一の方法は、削除して新しいものを作成することです。

StatefulWidget(状態を持つウィジェット)

一方、StatefulWidgetは内部状態を持ち、その状態の変更に応じてユーザーインターフェースを更新します。実装は以下のように2つのクラス(以下の例ではMyHomePage_MyHomePageState)で構成されます。

class MyHomePage extends StatefulWidget {  
  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);
  
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // カウンターは"状態"
  int counter = 0;
  
  void incrementCounter() {
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$counter'),
        Button(
          onPressed: incrementCounter,
        ),
      ],
    );
  } 
}

StatefulWidgetは少し複雑に見えますが、基本的な仕組みは以下のとおりです。

  1. 2つのクラスで構成される
    • StatefulWidgetクラス: ウィジェットの設定を定義
    • Stateクラス: 実際の状態と表示を管理
  2. クラスの役割
    • 上部のクラス(例: MyHomePage)はStatefulWidgetを継承し、createState()メソッドで対応するStateオブジェクトを作成します
    • 下部のクラス(例: _MyHomePageState)はStateを継承し、実際の状態(変数)と画面表示(buildメソッド)を管理します
  3. 状態の更新方法
    • 状態(例: counter変数)を変更するときはsetState()メソッドを使います
    • setState()を呼ぶとFlutterが自動的に画面を再描画してくれます
  4. 命名の慣習
    • StatefulWidgetの名前がHogeなら、対応するStateクラスは_HogeStateと命名するのが一般的です
    • 先頭の_(アンダースコア)はDartでプライベート(非公開)を意味します

buildメソッドはStatelessWidgetと同様に、画面に表示する内容を定義するメソッドです。違いはStatefulWidgetでは状態の変化に応じてbuildメソッドが再実行され、画面が更新される点にあります。

Reactとの比較

Flutterの状態管理の概念は、Reactなどの他のUIフレームワークと多くの類似点があります。実装方法を比較することで、両者の設計思想の共通点と相違点を理解できます。

・StatelessWidgetとReactの純粋コンポーネント
FlutterのStatelessWidgetは、Reactの純粋関数コンポーネント(Pure Function Component)に相当します。

Flutter(StatelessWidget):

class Greeting extends StatelessWidget {
  final String name;
  
  const Greeting({Key? key, required this.name}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Text('こんにちは、$name さん!');
  }
}
React(関数コンポーネント):
const Greeting = ({ name }) => {
  return <div>こんにちは、{name} さん!</div>;
};

どちらも外部から渡されたプロパティ(props)のみに依存し、内部状態を持ちません。入力が同じであれば常に同じ出力を返す「純粋」な性質を持っています。

StatefulWidgetとReactの状態管理

FlutterのStatefulWidgetはReactの状態を持つコンポーネントに相当します。Reactではクラスコンポーネントまたはフックを使用した関数コンポーネントで状態管理を実現します。

・カウンターアプリの例
Flutter (StatefulWidget):

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);
  
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;
  
  void increment() {
    setState(() {
      count++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('カウント: $count'),
        ElevatedButton(
          onPressed: increment,
          child: Text('増加'),
        ),
      ],
    );
  }
}
React (Hooks使用):
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>カウント: {count}>
      <button onClick={increment}>増加</button>
    </div>
  );
}

状態更新メカニズムの類似点

両フレームワークの状態更新メカニズムには、以下のような類似点があります。

  1. 宣言型UI: 両者とも宣言型のアプローチを採用しており「何を表示するか」に焦点を当てています
  2. 状態更新による再描画:
    • Flutter: setState()を呼び出すと、関連するStateオブジェクトが「dirty」としてマークされ、次のフレームでbuildメソッドが再実行されます。
    • React: setState()またはuseStateのセッター関数を呼ぶとコンポーネントの再レンダリングがスケジュールされます
  3. 単一方向データフロー: 両フレームワークともデータは親から子へと一方向に流れ、状態の変更は特定のメソッドを通じて行われます

ライフサイクルの概念

両フレームワークはコンポーネント/ウィジェットのライフサイクルという概念を持っています。

  • initState(): コンポーネントの初期化(ReactのcomponentDidMountuseEffect([], ())に相当)
     class CounterState extends State<Counter> {
        late Timer _timer;
        
        @override
        void initState() {
          super.initState();
          // ウィジェットが初めて作成されたときに一度だけ実行される
          _timer = Timer.periodic(Duration(seconds: 1), (timer) {
            setState(() {
              count++;
            });
          });
        }
      }
  • dispose(): コンポーネントの破棄(ReactのcomponentWillUnmountに相当)
      @override
      void dispose() {
        // ウィジェットがウィジェットツリーから削除されるときに呼ばれる
        _timer.cancel(); // リソースの解放
        super.dispose();
      }
  • didUpdateWidget(): プロパティ変更時(ReactのcomponentDidUpdateに相当)
      @override
      void didUpdateWidget(Counter oldWidget) {
        super.didUpdateWidget(oldWidget);
        // 親ウィジェットが再構築され、このウィジェットの設定が変更されたときに呼ばれる
        if (oldWidget.initialValue != widget.initialValue) {
          // プロパティが変更された場合の処理
          setState(() {
            count = widget.initialValue;
          });
        }
      }

相違点

主な相違点としては、FlutterはStatefulWidgetStateを明確に分離する設計を採用している点があります。これによりウィジェットの設定と状態管理の責務が分離され、コードの構造化が促進されます。

これに対して、Reactでは特にフックの導入によりコンポーネントと状態管理がより統合されたアプローチとなっています(※ただし、Flutterにおいてもフックを利用するアプローチが浸透しつつあり、Reactのフック型APIに近い形で状態管理を行うことも可能になっています)。

効率的な再描画

Flutterの状態管理システムは効率的な再描画を実現するように設計されています。独自のレンダリングエンジンにより変更された部分のみを特定し、必要最小限の更新を行います。これは、多くの場合Reactなどのフレームワークでしばしば必要となる手動の最適化(メモ化など)を不要とします。

StatelessWidgetStatefulWidgetの明確な区分は、この最適化プロセスを支援します。変わらない部品(StatelessWidget)と変わる部品(StatefulWidget)をはっきり分けることで「この部分は変わらないから、再計算しなくて良い」とFlutterが自動的に判断できるようになっています。

Reactのパフォーマンス最適化との比較

FlutterとReactでは、UIの更新処理に根本的な違いがあります。

  • Flutter: 独自のレンダリングエンジンが変更された部分を直接GPUに描画します。ウィジェットツリーの差分を計算し、必要な部分だけを再構築します。これは「レイヤーツリー」と呼ばれる低レベルの描画命令に変換されます
  • React: 仮想DOM(Virtual DOM)を使用し、実際のDOMとの差分を計算してから更新します。この2段階のプロセスは効率的ですが、DOMの操作自体が比較的重いため、Flutterの直接描画方式と比べると若干のオーバーヘッドが生じることがあります

Flutterのアプローチは、モバイルアプリケーションのような高パフォーマンスが求められる環境で特に効果的で、フレームワークは変更の追跡を効率的に行い、パフォーマンスを向上させることができます。

おわりに

今回は、実際にFlutterプロジェクトを作成し、その構造を解説しました。さらに、アプリケーション開発の要となる状態管理について学び、それがウィジェットとしてどのように表現されるかを詳しく見てきました。

次回は、この基礎的な理解を踏まえて、より実践的な内容に踏み込みながら、実例を交えつつ詳しく解説していく予定です。

株式会社Fivot「IDARE」事業部エンジニア
神戸市外国語大学卒業後、江戸時代より約300年続く老舗和菓子屋に入社。伊勢本店勤務を経て東京路面店出店事業に従事したのち退職。餅、餡、モバイルアプリの製造を強みとし、現在はFlutterを用いた開発を中心に担当。札幌在住、二児の父。
個人向け貯蓄アプリであるイデアは「貯まるキャッシュレス」というコンセプトのもと、「高還元ボーナス」「充実の貯蓄サポート機能」「使いやすいプリカ」という3つの軸でサービスを展開しています。2021年4月のサービス開始から3年でユーザーが設定している目標金額の総額は150億円を突破。多くのユーザーに支持され、事業を拡大しています。
公式サイト: https://idare.jp/

連載バックナンバー

開発言語技術解説
第2回

「Flutter」のプロジェクト構造と状態管理でアプリ開発を標準化する

2025/4/4
第2回の今回は、Flutterのプロジェクト構造と、Stateless/Statefulウィジェットによる状態管理の基本を解説します。
開発言語技術解説
第1回

なぜ「Flutter」なのか、そしてなぜ「Dart」なのか

2025/2/12
第1回の今回は、Flutterの概要とDartの特徴を紹介し、クロスプラットフォーム開発の利点と採用の理由を解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています