Wiki_Flutter_Package_Riverpod - inoueshinichi/Wiki_Flutter GitHub Wiki

Riverpodによる状態管理

参考

Riverpod パッケージ

  • 状態管理
  • 日本における開発事例が多い
  • Flutterの状態管理の思想からは外れている
  • 依存性の注入によるサービスロケータパターン

Riverpod関連のパッケージ

  • 基本機能を提供するパッケージ
  • Providerのコードを生成するパッケージ
  • 静的解析を行うパッケージ

基本機能を提供するパッケージ群

パッケージ名 内容
riverpod Flutterに依存しない
flutter_riverpod Flutter関連ならこれを使うべし
hooks_riverpod flutter_riverpod + Reactのフック

主機能

機能 内容
Provider 状態のキャッシュ
RefとWidgetRef 状態の提供
ConsumerWidget 状態を使う
  • ConsumerWidgetのbuild()メソッドの引数は, WidgetRefが渡されて, それを介して状態を取得する.
  • Providerの状態が変化するとbuild()メソッドが再度呼び出される.

最小構成のRiverpod

final greetProvider = Provider((ref: Ref) {
  return 'Hello Flutter';
});

class MyTestPage extends ConsumerWidget {
  const MyTestPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greet = ref.watch(greetProvider);
    return Center(child: Text('${greet}');
  }
}

NotifierとNotifierProviderを用いたRiverpod

  • build()メソッドで初期値を与える
  • 任意の更新関数を定義する
  • 状態変数はstate
// Provider
class CounterNotifier extends Notifier<T> {
  @override
  T build() => 0; // 初期値
  
  // 更新関数
  void increment() {
    state = state + 1;
  }
}

// NotifierProvider
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>() { return CounterNotifier(); });

// Consumer
class MyTestPage extends ConsumerWidget {
  const MyTestPage({super.key, required this.title});
  
  final String title;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterNotifierProvider);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("You have pushed the button this many times"),
            Text('$counter',
                 style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterNotifierProvider.notifier).increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Providerコードを生成するパッケージ(推奨)

  • 目的: Riverpodによるボイラーテンプレートを減らす
  • riverpod_generator
  • riverpod_annotation
  • build_runner

静的解析を行うパッケージ群(推奨)

  • riverpod_lint: custom_lintを内部で利用する

Riverpodに関するインストール

  • '$ flutter pub add --dev riverpod_generator riverpod_lint build_runner'
  • '# flutter pub add flutter_riverpod riverpod_annotation'
// コードを生成しない通常パターン
final greetProvider = Provider((ref: Ref) {
  return 'No generator for provider code';
});

// コード生成を使用するパターン
@riverpod
String greet(GreetRef ref) {
  return "Use generator for provider code';
});

Provider Generatorのメリット

  • Providerに関するコードを記述する際の意思決定が減る
  • Providerへ渡すパラメータの制限がなくなる
  • Providerの変更がホットリロードできる

関数ベースとクラスベース

関数ベース

// lib/test_func_provider.dart
part 'test_func_provider.g.dart';

@riverpod
String greet(GreetRef ref) {
  return 'Function base provider';
}
  • 生成後のProvider名はgreetProviderになる
  • 生成方法: $ flutter packages pub run build_runner build

クラスベース

  • _$ + クラス名のクラスを継承する
// lib/test_class_provider.dart
part 'test_class_provider.g.dart';

@riverpod
class CounterNotifier extends _$CounterNotifier {
//class CounterNotifier extends Notifiler<T>

  @override
  int build() => 0;

  void increment() {
    state = state + 1;
  }
}

非同期処理のProvider

  • Future型やStream型を提供するProvider

関数ベース(非同期)

@riverpod
Future<String> asyncGreet(AsyncGreetRef ref) async {
  await Future.delayed(const Duration(seconds: 1)); // python asincioのasyncio.sleep()と同じ
  return 'Async provider of riverpod with function base';
} // ConsumerWidgetで取得する際は, AsyncValue型でラップされる

クラスベース(非同期)

@riverpod
class CounterNotifier extends _$CounterNotifier {
  
  @override
  Future<int> build() async {
    await Future.delayed(const Duration(seconds: 1));
    return 0;
  } // ConsumerWidgetで取得する際は, AsyncValue型でラップされる
}

非同期Providerの戻り値はAsyncValue型

  • 非同期の値を安全に扱うためのヘルパークラス
  • AsyncValue
  • Tは状態を決めるキーワード: e.g. T=String: 'loading', 'error', 'data'

  • 状態: loading, error, data
// asyncGreetProviderを監視するWidget
class HomePage extends ConsumerWidget {
  const HomePage({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Providerの戻り値がFuture<String>ではなく, AsyncValue<T>クラスでラップされている.
    final AsyncValue<String> greet = ref.watch(asyncGreetProvider);
    
    return Center(
      child: greet.when(
        loading: () => const Text('Loading'),
        error: (e, st) => Text(e.toString()), 
        data: (greet) => Text(greet),
     ),
    );
  }
}

クラスベースの非同期型Providerの更新にもAsyncValueを用いる

@riverpod
class CounterNotifier extends _$CounterNotifier {
  
  @override
  Future<int> build() async {
    await Future<void>.delayed(const Duration(seconds: 1));
    return 0;
  }
  
  void increment() async {
    final currentValue = state.valueOrNull;

    // AsyncValueがdata以外はnullが返る
    if (currentValue == null) {
      return;
    }
    
    state = const AsyncLoading(); // 状態をloadingにする
    await Future<void>.delayed(const Duration(seconds:1));
    state = AsyncValue.data(currentValue + 1);
  }
}
  • AsyncValue型は, 次の3つの状態のどれかをとる. loading, error, data

Raw型によるラッピングでAsyncValue型の使いづらさを解消

  • AsyncValue型は, 複数の非同期Providerが絡むと使いづらい
// e.g. 使いづらい例

// async provider: 1
@riverpod
Future<int> fakeFirstApi(FakeFirstApiRef ref) async {
  await Future.delayed(const Duration(seconds: 1));
  return 1;
} // fakeFirstApiProvider

// async provider: 2
@riverpod
Future<int> fakeSecondApi(FakeSecondApiRef ref) async {
  await Future.delayed(const Duration(seconds: 1));
  return 2;
} // fakeSecondApiProvider

// integrated async provider with 1 and 2
@riverpod
Future<int> fakeSumApi(FakeSumApiRef ref) async {
  // AsyncValue<int>が扱いづらい
  final AsyncValue<int> firstApiResult = ref.watch(fakeFirstApiProvider);
  final AsyncValue<int> secondApiResult = ref.watch(fakeSecondApiProvider);
  await Future.delayed(const Duration(seconds: 1));
  return firstApiResult.data + secondApiResult.data;
} // fakeSumApiProvider
  • 解決策: Providerの戻り値の型がAsyncValueではなく, Futureだと良い.
  • Providerの戻り値であるFutureをRaw型でラップすると, 呼び出し側でFutureが得られる.
// e.g. 改良版

// async provider: 1
@riverpod
Raw<Future<int>> fakeFirstApi(FakeFirstApiRef ref) async {
  await Future.delayed(const Duration(seconds: 1));
  return 1;
} // fakeFirstApiProvider

// async provider: 2
@riverpod
Raw<Future<int>> fakeSecondApi(FakeSecondApiRef ref) async {
  await Future.delayed(const Duration(seconds: 1));
  return 2;
} // fakeSecondApiProvider

// integrated async provider with 1 and 2
@riverpod
Raw<Future<int>> fakeSumApi(FakeSumApiRef ref) async {
  final int firstApiResult = await ref.watch(fakeFirstApiProvider);
  final int secondApiResult = await ref.watch(fakeSecondApiProvider);
  await Future.delayed(const Duration(seconds: 1));
  return firstApiResult + secondApiResult;
} // fakeSumApiProvider

Providerから値を取得する方法

  • ref.watch()メソッド
  • ref.read()メソッド

watch()メソッド

  • build()メソッド内部でwatchするとProviderの状態が変化した場合に, build()メソッドが走る.
  • 複数のwatch()メソッドによる状態変化のループによって, 無限ループに陥らないように注意すること.

read()メソッド

  • その時点でのProviderの状態を取得する
  • build()メソッドは走らない.
  • onPress()内部などユーザートリガーのイベントやStateのライフサイクルイベントで使う.

select()メソッドによる不要な再描画を防ぐ

  • 状態(集合)のうち, 指定した一部の要素だけ変化した場合にbuild()メソッドを走らせることで再描画を極力減らす.
// 状態 Point
class Point {
  Point(this.x, this.y);
  int x;
  int y;
}

// Provider
@riverpod
class AsyncPointNotifier extends _$AsyncPointNotifier {

  @override
  Future<Point> build() {
    await Future<void>.delayed(const Duration(seconds: 1));
    return Point(0,0);
  }
  
  @override
  void plusOne() async {
    final currentValue = state.valueOrNull;
    if (currentValue == null) {
      return;
    }
    
    state = const AsyncLoading();
    await Future<void>.delayed(const Duration(seconds: 1));
    state = AsyncValue.data(Point(currentValue.x + 1, currentValue.y + 1));
  }
}

// Providerを使う側
class HomePage extends Consumerwidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final x = await ref.watch(asyncPointNotifier.select((point) => point.x));
    ...
  }
}

Providerのライフサイクル

  • コード生成したProviderは, 購読されなくなるとGCで消される.
  • 複数の画面を跨いで状態を共有したいケースでは, 常にメモリ上にProviderを配置しておく.
  • アプリ起動中, 常にメモリ上にProviderを配置したいケースでは@Riverpotを用いる.

GCの削除スコープからProviderを外す方法

  • @Riverpodを用いる
@Riverpod(keepAlive: true)
class AsyncCounterNotifier extends _$AsyncCounterNotifier {
  @override
  Future<int> build() => 0;

  void increment() async {
    final currentValue = state.valueOrNull;
    if (currentValue == null) {
      return;
    }
    
    state = const AsyncLoading(); // loading state
    await Future<void>.delayed(const Duration(seconds: 1));
    state = AsyncValue.data(currentValue + 1);
  }
}

任意のタイミングでProviderを再構築したい場合

class HomePage extends ConsumerWidget {
  HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ...
    ref.refresh(counterNotifierProvider);
    ...
  }
}

Providerに値を渡す方法

関数ベース

@riverpod
String greet(GreetRef ref, String message) {
  return 'Hello $message';
}

@riverpod
Raw<Future<String>> asyncGreet(AsyncGreet ref, String message) async {
  await Future<void>.delayed(const Duration(seconds: 1));
  return 'Hello $message';
}

クラスベース

@riverpod
class AsyncGreetNotifier extends _$AsyncGreetNotifier {
  
  @override
  Future<String> build(String message) async {
    await Future<void>.delayed(const Duration(seconds: 1));
    return 'Good $message';
  }

  void asyncAddName(String name) async {
    final currentValue = state.valueOrNull;
    if (currentValue == null) {
      return;
    }

    state = const AsyncLoading(); // state -> loading
    await Future<void>.delayed(const Duration(seconds: 1));
    state = AsyncValue.data("${currentValue}, ${name}!");
  }
}

利用側

class HomePage extends ConsumerWidget {
  const HomePage({super.key, required this.name});

  final String name;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncGreetNotifier = asyncGreetNotifierProvider('morning'); // Good morning
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Async greeting demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAligment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have greeted below your friends.'),
            Text(
              "${ref.watch(asyncGreetNotifier).switch(
                loading: () => const Text('Loading'),
                error: (e, st) => Text(e.toString()),
                data: (greetToFriends) => Text('$greetToFriends'),
              )",
              style: theme.of(context).textTheme.headlineMedium,
            ),
         ],
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () async {
          await ref.read(counterNotifier.notifier).asyncAddName("Ben"); // Future<void>
       },
       tooltip: "Add friend",
       child: const Icon(Icons.add),
     ),
   );
  }
}
⚠️ **GitHub.com Fallback** ⚠️