Flutterアプリで状態を管理する6つの方法(コード例付き)

Flutterで状態を管理する方法

目次

この記事では、Flutter アプリで状態を管理する6つの人気のある方法について、実際の例とベストプラクティスを紹介します:

状態管理 は、Flutter開発において最も重要で、議論が続くトピックの1つです。これは、アプリがデータの変更をどのように処理し、UIを効率的に更新するかを決定します。Flutterは、単純なものから非常にスケーラブルなものまで、状態管理のためのいくつかの戦略を提供しています。

Flutterにおける状態管理の戦略は以下の通りです:

  1. 🧱 setState() — 内蔵で、最もシンプルなアプローチ
  2. 🏗️ InheritedWidget — Flutterの状態伝播のための基盤
  3. 🪄 Provider — 多くのアプリに推奨されるソリューション
  4. 🔒 Riverpod — Providerの現代的でコンパイル安全な進化版
  5. 📦 Bloc — スケーラブルで、企業向けのアプリケーション向け
  6. GetX — 軽量で、オールインワンのソリューション

flutter troubles mega tracktor


1. setState()を使用する — 基本

Flutter で状態を管理する最も簡単な方法は、StatefulWidget内で内蔵のsetState()メソッドを使用することです。

このアプローチは、ローカルUI状態 — 1つのウィジェットに属し、アプリ全体で共有する必要がない状態 — に最適です。

例: setState()を使用したカウンターアプリ

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('setState Counter')),
      body: Center(
        child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

利点

  • 実装が非常に簡単
  • ローカルまたは一時的なUI状態に最適
  • 外部依存がない

欠点

  • 大規模なアプリではスケーリングが難しい
  • ウィジェット間で状態を共有するのが難しい
  • ロジックとUIが混在している

使用するべき状況

  • プロトタイピングまたは小さなウィジェットの作成
  • 離れたUI状態(例:ボタンのトグル、モーダルの表示)の処理

2. InheritedWidget — Flutterの基盤

InheritedWidget は、Flutterがウィジェットツリーにデータを伝播させるための低レベルのメカニズムです。多くの状態管理ソリューション(Providerを含む)はこれの上に構築されています。

InheritedWidgetを理解することで、Flutterの状態管理がどのように内部で動作するかを把握できます。

例: InheritedWidgetを使用したテーママネージャー

import 'package:flutter/material.dart';

// InheritedWidgetでテーマデータを保持する
class AppTheme extends InheritedWidget {
  final bool isDarkMode;
  final Function toggleTheme;

  const AppTheme({
    Key? key,
    required this.isDarkMode,
    required this.toggleTheme,
    required Widget child,
  }) : super(key: key, child: child);

  // ワイジェットツリーの上にあるAppThemeにアクセスするメソッド
  static AppTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppTheme>();
  }

  @override
  bool updateShouldNotify(AppTheme oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

// 状態変更を管理するStateful Wrapper
class ThemeManager extends StatefulWidget {
  final Widget child;

  const ThemeManager({Key? key, required this.child}) : super(key: key);

  @override
  _ThemeManagerState createState() => _ThemeManagerState();
}

class _ThemeManagerState extends State<ThemeManager> {
  bool _isDarkMode = false;

  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return AppTheme(
      isDarkMode: _isDarkMode,
      toggleTheme: _toggleTheme,
      child: widget.child,
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeManager(
      child: Builder(
        builder: (context) {
          final theme = AppTheme.of(context);
          return MaterialApp(
            theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
            home: HomeScreen(),
          );
        },
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = AppTheme.of(context);
    
    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Theme')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => theme.toggleTheme(),
              child: Text('Toggle Theme'),
            ),
          ],
        ),
      ),
    );
  }
}

利点

  • Flutterに組み込まれている — 外部依存がない
  • ウィジェットの再構築が効率的
  • 他のソリューションを理解するための基盤
  • 伝播ロジックの直接的な制御

欠点

  • ブーリュープレートコードが冗長
  • ラッパーウィジェットのStatefulWidgetが必要
  • 間違いを起こしやすい
  • 初心者向けではない

使用するべき状況

  • Flutterの状態管理がどのように内部で動作するかを学ぶ
  • カスタム状態管理ソリューションを構築する
  • 状態伝播に非常に具体的な制御が必要な場合

3. Provider — Flutter推奨ソリューション

アプリの状態が複数のウィジェット間で共有される必要がある場合、Provider が助けになります。

Provider制御の逆転 に基づいています — ウィジェットが状態を所有する代わりに、プロバイダが他のコンポーネントに状態を公開します。Flutterチームは、中規模 アプリに対して公式にこれを推奨しています。

セットアップ

pubspec.yaml にこれを追加します:

dependencies:
  provider: ^6.0.5

例: Providerを使用したカウンターアプリ

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.read<CounterModel>().increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

利点

  • リアクティブで効率的な更新
  • UIとロジックの明確な分離
  • 良く文書化されており、コミュニティのサポートがある

欠点

  • setState() よりもわずかに多くのブーリュープレートコード
  • ネストされたプロバイダが複雑になる可能性がある

使用するべき状況

  • 複数のウィジェット間で状態を共有する必要がある
  • 複雑さがないリアクティブなパターンが必要
  • アプリがプロトタイプサイズを超えて成長している

4. Riverpod — Providerの現代的進化

Riverpod は、BuildContextへの依存を除去し、コンパイル時の安全性を追加したProviderの完全なリライトです。Providerの制限を解決しつつ、同じ哲学を維持するように設計されています。

セットアップ

pubspec.yaml にこれを追加します:

dependencies:
  flutter_riverpod: ^2.4.0

例: Riverpodを使用したユーザー情報

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// モデル
class UserProfile {
  final String name;
  final int age;
  
  UserProfile({required this.name, required this.age});
  
  UserProfile copyWith({String? name, int? age}) {
    return UserProfile(
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
}

// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
  ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));

  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
}

// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
  return ProfileNotifier();
});

// Computed provider
final greetingProvider = Provider<String>((ref) {
  final profile = ref.watch(profileProvider);
  return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ProfileScreen());
  }
}

class ProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final profile = ref.watch(profileProvider);
    final greeting = ref.watch(greetingProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Profile')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(greeting, style: TextStyle(fontSize: 20)),
            SizedBox(height: 30),
            TextField(
              decoration: InputDecoration(labelText: 'Name'),
              onChanged: (value) {
                ref.read(profileProvider.notifier).updateName(value);
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    ref.read(profileProvider.notifier).updateAge(profile.age + 1);
                  },
                  child: Text('+'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    if (profile.age > 0) {
                      ref.read(profileProvider.notifier).updateAge(profile.age - 1);
                    }
                  },
                  child: Text('-'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

利点

  • BuildContextへの依存がない — どこからでも状態にアクセス可能
  • コンパイル時の安全性 — ランタイム前にエラーを検出可能
  • 高いテスト性
  • 強力な状態の構成
  • メモリリークがない

欠点

  • 伝統的なFlutterパターンとは異なるAPI
  • Providerより学習曲線が急
  • コミュニティが小さい(ただし急速に成長中)

使用するべき状況

  • 新しいFlutterプロジェクトを開始する
  • 型安全性とコンパイル時の保証が必要
  • 複雑な状態依存と構成が必要
  • 中規模から大規模なアプリケーションで作業する

5. BLoC(ビジネスロジックコンポーネント) — 企業向けアプリ

BLoC パターン は、ストリーム を使用してビジネスロジックをUIから完全に分離する、より高度なアーキテクチャです。

これは、大規模または企業向けのアプリケーション に最適で、予測可能でテスト可能な状態遷移が不可欠な場面に適しています。

セットアップ

プロジェクトにこの依存関係を追加します:

dependencies:
  flutter_bloc: ^9.0.0

例: BLoCを使用したカウンターアプリ

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// イベント
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(home: CounterScreen()),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('Count: $count', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

利点

  • 複雑なアプリケーションに対してスケーラブルで保守性が高い
  • レイヤーの明確な分離
  • 単体テストが簡単

欠点

  • ブーリュープレートコードが多い
  • 学習曲線が急

使用するべき状況

  • 企業向けまたは長期プロジェクトを構築する
  • 予測可能でテスト可能なロジックが必要
  • 複数の開発者が異なるアプリケーションモジュールに作業する

6. GetX — 軽量なオールインワンソリューション

GetX は、ルーティング、依存性注入、ユーティリティを含む、最小限のブーリュープレートを持つ強力な状態管理ソリューションです。すべてのソリューションの中で最もブーリュープレートが少ないことで知られています。

セットアップ

pubspec.yaml にこれを追加します:

dependencies:
  get: ^4.6.5

例: GetXを使用した買い物リスト

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// コントローラー
class ShoppingController extends GetxController {
  // 観測可能なリスト
  var items = <String>[].obs;
  
  // 観測可能なカウンター
  var totalItems = 0.obs;

  void addItem(String item) {
    if (item.isNotEmpty) {
      items.add(item);
      totalItems.value = items.length;
    }
  }

  void removeItem(int index) {
    items.removeAt(index);
    totalItems.value = items.length;
  }

  void clearAll() {
    items.clear();
    totalItems.value = 0;
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Shopping List',
      home: ShoppingScreen(),
    );
  }
}

class ShoppingScreen extends StatelessWidget {
  // コントローラーを初期化
  final ShoppingController controller = Get.put(ShoppingController());
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Shopping List'),
        actions: [
          Obx(() => Padding(
                padding: EdgeInsets.all(16),
                child: Center(
                  child: Text(
                    'Items: ${controller.totalItems}',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              )),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    decoration: InputDecoration(
                      hintText: 'Enter item name',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    controller.addItem(textController.text);
                    textController.clear();
                  },
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ),
          Expanded(
            child: Obx(() {
              if (controller.items.isEmpty) {
                return Center(
                  child: Text('No items in your list'),
                );
              }
              
              return ListView.builder(
                itemCount: controller.items.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: CircleAvatar(child: Text('${index + 1}')),
                    title: Text(controller.items[index]),
                    trailing: IconButton(
                      icon: Icon(Icons.delete, color: Colors.red),
                      onPressed: () => controller.removeItem(index),
                    ),
                  );
                },
              );
            }),
          ),
          if (controller.items.isNotEmpty)
            Padding(
              padding: EdgeInsets.all(16),
              child: ElevatedButton(
                onPressed: () {
                  Get.defaultDialog(
                    title: 'Clear List',
                    middleText: 'Are you sure you want to clear all items?',
                    textConfirm: 'Yes',
                    textCancel: 'No',
                    onConfirm: () {
                      controller.clearAll();
                      Get.back();
                    },
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('Clear All'),
              ),
            ),
        ],
      ),
    );
  }
}

利点

  • ブーリュープレートが最小限 — 開発が速い
  • 状態、ルーティング、DI、スナックバー、ダイアログのオールインワンソリューション
  • BuildContextが必要ない
  • 非常に軽量で高速
  • 学習が簡単

欠点

  • Provider/BLoCに比べて「Flutterらしい」感じが少ない
  • 注意しないと結合が強くなる可能性がある
  • Providerよりエコシステムが小さい
  • 一部の開発者は「あまりにもマジカル」だと感じる

使用するべき状況

  • プロトタイピングまたはMVP開発
  • 小規模から中規模のアプリ
  • ブーリュープレートが最小限であることが優先
  • シンプルさをチームが好む

適切な状態管理ソリューションの選択

ソリューション 複雑さ ブーリュープレート 学習曲線 最適な用途
🧱 setState() 最小 容易 簡単なローカル状態、プロトタイプ
🏗️ InheritedWidget Flutter内部の動作を学ぶ、カスタムソリューション
🪄 Provider 低〜中 容易 多くのアプリ、共有状態
🔒 Riverpod 低〜中 現代的なアプリ、型安全性
📦 BLoC 企業向けアプリ、複雑なビジネスロジック
GetX 最小 容易 ラピッド開発、MVP

迅速な決定ガイド:

アプリの複雑さ 推奨されるアプローチ 例の使用ケース
🪶 簡単 setState() または GetX ボタンのトグル、アニメーション、小さなウィジェット
⚖️ 中 Provider または Riverpod 共有テーマ、ユーザー設定、データキャッシュ
🏗️ 複雑 BLoC または Riverpod イコマース、チャットアプリ、金融ダッシュボード

最後の考え

Flutter における状態管理には、万能なソリューション は存在しません。

実用的なアプローチ:

  • シンプルなウィジェットやプロトタイプ には setState() から始める
  • Flutterの内部動作を理解する ために InheritedWidget を学ぶ
  • アプリが成長し共有状態が必要になる と Provider に進む
  • 型安全性と現代的なパターンが重要 な新規プロジェクトでは Riverpod を検討する
  • 大規模な企業向けアプリケーションと複雑なビジネスロジック には BLoC を採用する
  • 迅速な開発と最小限のブーリュープレートが優先 される場合は GetX を試す

重要なポイント:

✅ 過剰なエンジニアリングはしない — setState() を必要以上に使わない
✅ Provider と Riverpod は多くのアプリケーションに最適
✅ BLoC は大規模チームと複雑なアプリケーションに最適
✅ GetX は速さに優れているが、結合に注意が必要
✅ InheritedWidget を理解することで、どのソリューションもマスターできる

鍵は、シンプルさ、スケーラビリティ、保守性 のバランスを取ること — そして、あなたの特定のニーズ、チームの専門性、プロジェクトの要件に合った正しいツールを選ぶことです。

Flutter状態管理ライブラリのリンク

その他の役に立つリンク