2026年のFlutter状態管理:Riverpod vs Bloc vs GetX 徹底比較ガイド

Riverpod 3.0、Bloc 9.0、GetXの3大Flutter状態管理ソリューションを、コード例・パフォーマンス・テスト戦略の観点から徹底比較します。

Flutter状態管理 2026年版:Riverpod、Bloc、GetXの比較図

Flutter状態管理は、ウィジェット間のデータフローを制御するアプリケーション設計の中核的要素である。2026年現在、Flutterエコシステムでは3つのソリューションが主流を占める。コンパイル時安全性を実現したRiverpod 3.0、エンタープライズ向けイベント追跡機能を備えたBloc 9.0、そして採用率が低下しながらも既存プロジェクトに残存するGetXである。適切なソリューションの選択は、テスト容易性、拡張性、長期的な保守コストに直接影響を与える。

選択の判断基準

Riverpod 3.0は、コンパイル時安全性とボイラープレートの少なさから、大半のプロジェクトに最適である。Bloc 9.0は、金融や医療など監査証跡が必要な規制産業の標準的選択肢となっている。GetXは、移行予算のない既存コードベースの保守にのみ検討すべきである。

Riverpod 3.0:コンパイル時安全性と自動リトライ機構

Riverpod 3.0は、Flutterアプリケーションにおける状態の宣言と消費の方法を根本的に変革した。アノテーションベースのコード生成により、依存関係のエラーがランタイムではなくコンパイル時に検出される。これにより、従来は手動テストでしか発見できなかったバグの一群が完全に排除される。

自動リトライ機構は、プロバイダーの計算が失敗した場合にネットワークの一時的なエラーを自動で処理する。設定可能な遅延を用いたリトライにより、エラーリカバリー用のボイラープレートコードが大幅に削減される。

counter_provider.dartdart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_provider.g.dart';

// Code generation ensures compile-time safety

class Counter extends _$Counter {
  
  int build() => 0; // Initial state

  void increment() => state = state + 1;
  void decrement() => state = state - 1;
  void reset() => state = 0;
}

@riverpodアノテーションにより、プロバイダーのボイラープレートがすべて自動生成される。型の不一致、オーバーライドの欠落、循環依存はコンパイル段階で検出される。

user_repository_provider.dartdart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_repository_provider.g.dart';


Future<User> currentUser(Ref ref) async {
  final authService = ref.watch(authServiceProvider);
  final userId = authService.currentUserId;

  // Auto-retry on network failure (Riverpod 3.0)
  final response = await ref.watch(
    httpClientProvider,
  ).get('/api/users/$userId');

  return User.fromJson(response.data);
}

Riverpod 3.0では、ウィジェットが画面外に移動した際にプロバイダーのリスナーが自動的に一時停止する。これにより不要な計算が減少し、モバイルデバイスのバッテリー消費が改善される。

Bloc 9.0:エンタープライズ向けイベント駆動アーキテクチャ

Bloc 9.0は、イベント・状態・ビジネスロジックの厳密な分離を強制する。すべての状態変更が特定のイベントにマッピングされるため、規制産業が要求する監査証跡が自動的に生成される。バージョン9.0で導入されたマウント安全性チェックにより、破棄されたウィジェット上でコールバックが実行されることが防止される。

authentication_event.dartdart
sealed class AuthenticationEvent {}

final class LoginRequested extends AuthenticationEvent {
  final String email;
  final String password;
  LoginRequested({required this.email, required this.password});
}

final class LogoutRequested extends AuthenticationEvent {}

final class SessionRestored extends AuthenticationEvent {
  final String token;
  SessionRestored({required this.token});
}

Dart 3のsealed classにより、イベントの網羅的パターンマッチングが保証される。コンパイラが、すべてのイベント型に対応するハンドラの存在を検証する。

authentication_bloc.dartdart
import 'package:flutter_bloc/flutter_bloc.dart';

class AuthenticationBloc
    extends Bloc<AuthenticationEvent, AuthenticationState> {
  final AuthRepository _authRepo;
  final TokenStorage _tokenStorage;

  AuthenticationBloc({
    required AuthRepository authRepo,
    required TokenStorage tokenStorage,
  })  : _authRepo = authRepo,
        _tokenStorage = tokenStorage,
        super(AuthenticationInitial()) {
    on<LoginRequested>(_onLoginRequested);
    on<LogoutRequested>(_onLogoutRequested);
    on<SessionRestored>(_onSessionRestored);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthenticationState> emit,
  ) async {
    emit(AuthenticationLoading());
    try {
      final token = await _authRepo.login(
        email: event.email,
        password: event.password,
      );
      await _tokenStorage.save(token);
      emit(AuthenticationSuccess(token: token));
    } catch (e) {
      emit(AuthenticationFailure(message: e.toString()));
    }
  }

  Future<void> _onLogoutRequested(
    LogoutRequested event,
    Emitter<AuthenticationState> emit,
  ) async {
    await _tokenStorage.clear();
    emit(AuthenticationInitial());
  }

  Future<void> _onSessionRestored(
    SessionRestored event,
    Emitter<AuthenticationState> emit,
  ) async {
    emit(AuthenticationSuccess(token: event.token));
  }
}

各イベントハンドラは明確な状態遷移を生成する。ロギングミドルウェアにより、デバッグやコンプライアンス目的ですべてのイベントを記録することが可能である。Bloc 9.0のEmittableStateStreamableSourceインターフェースは、軽量なモック実装を可能にしテストを簡素化する。

Blocイベントトランスフォーマー:高頻度入力の処理

Blocには、一般的な並行処理の問題を解決する組み込みイベントトランスフォーマーが用意されている。インクリメンタルサーチ、連続ボタンタップ、リアルタイムデータストリームの処理が宣言的に実現できる。

search_bloc.dartdart
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  final SearchRepository _repository;

  SearchBloc({required SearchRepository repository})
      : _repository = repository,
        super(SearchInitial()) {
    // restartable() cancels previous search on new input
    on<SearchQueryChanged>(
      _onQueryChanged,
      transformer: restartable(),
    );
    // droppable() ignores events while processing
    on<SearchResultSelected>(
      _onResultSelected,
      transformer: droppable(),
    );
  }

  Future<void> _onQueryChanged(
    SearchQueryChanged event,
    Emitter<SearchState> emit,
  ) async {
    if (event.query.length < 3) {
      emit(SearchInitial());
      return;
    }
    emit(SearchLoading());
    final results = await _repository.search(event.query);
    emit(SearchLoaded(results: results));
  }

  Future<void> _onResultSelected(
    SearchResultSelected event,
    Emitter<SearchState> emit,
  ) async {
    emit(SearchNavigating(result: event.result));
  }
}

restartable()トランスフォーマーは、新しい入力が到着した時点で進行中の検索をキャンセルし、古い結果が新しい結果を上書きすることを防止する。droppable()トランスフォーマーは、ナビゲーション処理中の重複タップを無視する。

Flutterの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

GetX:技術的負債と移行の現実

GetXは、高速なプロトタイピングと最小限のボイラープレートで人気を博した。しかし2026年現在、ライブラリは保守上の危機に直面している。散発的なアップデート、単一メンテナーへの依存、そして最新Flutter SDKとの互換性の問題が増加している。本番環境のGetXアプリケーションでは、コントローラーのライフサイクル問題や暗黙的グローバルシングルトンによるメモリリークが報告されている。

counter_controller.dart (GetX pattern)dart
import 'package:get/get.dart';

// Global singleton - difficult to test and scope
class CounterController extends GetxController {
  final count = 0.obs; // Reactive observable

  void increment() => count.value++;
  void decrement() => count.value--;

  // Lifecycle hooks - disposal timing is unpredictable
  
  void onClose() {
    // Cleanup may not execute reliably
    super.onClose();
  }
}

// Usage in widget
class CounterPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // Get.put creates a global singleton
    final controller = Get.put(CounterController());
    return Obx(() => Text('${controller.count}'));
  }
}

Get.put()はコントローラーをグローバルシングルトンとして登録する。複雑なナビゲーションフローでは、コントローラーが意図したスコープを超えて存続し、メモリを消費する。.obsリアクティブ変数はFlutterの標準的な状態通知システムをバイパスするため、他のパッケージとの統合が不安定になる。

GetXからRiverpodへの段階的移行手順

GetXコードベースを保守するチームにとって、Riverpodへの移行は段階的に実施可能である。両ライブラリは同一プロジェクト内で共存できるため、全面的な書き換えを行わずに画面単位で変換を進めることができる。

dart
// Step 1: Replace GetX controller with Riverpod notifier
// Before (GetX)
class ProductController extends GetxController {
  final products = <Product>[].obs;
  final isLoading = false.obs;

  Future<void> loadProducts() async {
    isLoading.value = true;
    products.value = await ProductApi.fetchAll();
    isLoading.value = false;
  }
}

// After (Riverpod 3.0)

class ProductList extends _$ProductList {
  
  Future<List<Product>> build() async {
    // Auto-retry on failure, auto-pause when off-screen
    return ProductApi.fetchAll();
  }

  Future<void> refresh() async {
    ref.invalidateSelf();
  }
}
dart
// Step 2: Replace widget bindings
// Before (GetX)
class ProductPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final ctrl = Get.put(ProductController());
    return Obx(() {
      if (ctrl.isLoading.value) return CircularProgressIndicator();
      return ListView.builder(
        itemCount: ctrl.products.length,
        itemBuilder: (_, i) => ProductTile(ctrl.products[i]),
      );
    });
  }
}

// After (Riverpod 3.0)
class ProductPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productListProvider);
    return productsAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => ErrorDisplay(error: err),
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) => ProductTile(products[i]),
      ),
    );
  }
}

Riverpod版では、AsyncValue.when()を通じてローディング・エラー・データの各状態を明示的に処理する。グローバルシングルトンも手動のライフサイクル管理も不要であり、ウィジェットのアンマウント時に自動的に破棄される。

パフォーマンス比較:リビルド効率

リビルド効率はフレームレートに直接影響する。各ソリューションはウィジェットのリビルドを異なる方法で処理し、その差異は数百アイテムを含むリストで顕著に現れる。

| 指標 | Riverpod 3.0 | Bloc 9.0 | GetX | |------|-------------|----------|------| | 選択的リビルド | select()フィルター | BlocSelector | フィールド毎の.obs | | コンパイル時安全性 | 完全(コード生成) | 部分的(sealed class) | なし | | 自動破棄 | 組み込み | close()による手動 | 不安定 | | 画面外での一時停止 | 自動(3.0) | 手動 | 非対応 | | イベント追跡 | Provider Observer | 完全なイベントログ | なし | | テスト分離 | ProviderContainer.test() | EmittableStateStreamableSource | Get.testModeが必要 | | バンドルサイズ | 約45KB | 約38KB | 約120KB(ルーティング・DI・HTTP含む) |

Riverpodのselect()メソッドとBlocのBlocSelectorは、変更されたデータに依存するウィジェットサブツリーのみを更新する外科的なリビルドを実現する。GetXの.obsもフィールド単位で同様の粒度を達成するが、依存関係グラフのコンパイル時検証が欠如している。

GetXのバンドルサイズに関する注意

GetXはルーティング、依存性注入、HTTPクライアント、状態管理を単一パッケージにバンドルしている。状態管理のみを使用するアプリケーションでも、120KB全体のライブラリをインポートすることになる。RiverpodとBlocは単一の責務に特化した軽量パッケージである。

各ソリューションのテスト戦略

テスト容易性は、チーム規模が拡大した際にどのソリューションがスケールするかを左右する重要な要素である。各ライブラリはテストに対するアプローチが異なる。

dart
// Riverpod test - isolated container
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';

void main() {
  test('Counter increments', () {
    final container = ProviderContainer.test();
    // Override dependencies for isolation
    final counter = container.read(counterProvider.notifier);

    expect(container.read(counterProvider), 0);
    counter.increment();
    expect(container.read(counterProvider), 1);
  });
}
dart
// Bloc test - event-driven verification
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  blocTest<AuthenticationBloc, AuthenticationState>(
    'emits [loading, success] on valid login',
    build: () => AuthenticationBloc(
      authRepo: MockAuthRepo(),
      tokenStorage: MockTokenStorage(),
    ),
    act: (bloc) => bloc.add(
      LoginRequested(email: 'dev@test.com', password: 'secure123'),
    ),
    expect: () => [
      isA<AuthenticationLoading>(),
      isA<AuthenticationSuccess>(),
    ],
  );
}

RiverpodのProviderContainer.test()はテストごとに分離された依存関係グラフを生成する。BlocのblocTestヘルパーは、イベント駆動アーキテクチャに対応した正確な状態遷移シーケンスを検証する。GetXのテストではGet.testMode = trueの設定とコントローラーライフサイクルの手動管理が必要であり、CI環境で不安定なテスト結果を招きやすい。

面接対策のポイント

Flutter状態管理は、モバイル開発者の面接で最も頻繁に問われるトピックの一つである。Riverpod、Bloc、GetXそれぞれのトレードオフを理解し、各ソリューションが適合するケースと適合しないケースを説明できることが、アーキテクチャ的な成熟度を示す。

判断マトリクス:最適なソリューションの選択

プロジェクトの制約条件が最適な選択を決定する。チーム規模、規制要件、既存コードベースのすべてが判断に影響する。

Riverpod 3.0は、コンパイル時安全性を重視するチーム、自動エラーリカバリーを伴う非同期データ取得が必要なプロジェクト、またはゼロから構築するコードベースに適している。学習曲線は中程度で、Providerに慣れた開発者は自然に移行できる。

Bloc 9.0は、規制産業(フィンテック、ヘルスケア)で運用されるプロジェクト、監査のための完全なイベント追跡が必要なチーム、または決済処理のような複雑な並行ワークフローを扱うアプリケーションに適している。ボイラープレートのコストは、大規模な保守性で回収される。

GetXは、移行コストが利用可能な予算を超える既存GetXコードベースの保守にのみ適している。2026年にGetXで新規プロジェクトを開始することは、初日から技術的負債を生み出す。Flutter公式ドキュメントは、推奨ソリューションの一覧にGetXを含めていない。

Flutter状態管理パターンのより深い学習には、状態管理の基礎モジュールが面接で問われる基本概念をカバーしている。プロバイダーパターンモジュールは、3つのソリューションすべてに適用される依存性注入戦略を解説している。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

まとめ

  • Riverpod 3.0は、コード生成によるコンパイル時安全性、失敗したプロバイダーの自動リトライ、モバイルデバイスのバッテリー消費を削減する一時停止・再開機能を提供する
  • Bloc 9.0は、完全な監査機能を備えたイベント駆動型状態遷移を強制し、規制産業におけるエンタープライズアプリケーションの標準となっている
  • GetXは、散発的なアップデートとSDK互換性の問題により2026年に保守上の危機に直面しており、既存GetXプロジェクトはRiverpodへの段階的移行を計画すべきである
  • GetXからRiverpodへの移行は、両ライブラリが同一プロジェクト内で共存できるため、画面単位で全面書き換えなしに進められる
  • テスト分離には大きな違いがある:RiverpodはProviderContainer.test()、BlocはblocTestによるイベントシーケンス検証、GetXは脆弱なグローバルテストモードの設定が必要となる
  • バンドルサイズはモバイルで重要である:Riverpod(約45KB)とBloc(約38KB)は特化型パッケージを提供する一方、GetX(約120KB)は未使用の機能もバンドルする

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#flutter
#state-management
#riverpod
#bloc
#getx

共有

関連記事