🚀

【Flutter】個人的Riverpodの理解まとめ

2023/09/11に公開

~Riverpodとは何か~

Flutterアプリケーションの状態管理を行うためのパッケージ。
https://riverpod.dev/ja/

共通の設定

⭐ProviderScopeの追加

sample
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

~各Widgetについて~

Consumer

⭐️Consumer内でrefが参照できる。
⭐️影響範囲が小さい。

sample
Consumer(builder: (context, ref, child) {
  return Text(
    '${ref.watch(_hitsujiCounterProvider)}',
  );
}),

ConsumerWidget

⭐️buildメソッド内でrefが参照できる。
⭐️Riverpod版のStatelessWidget。

sample
class MyHomePage extends ConsumerWidget {
  MyHomePage({super.key});

  final _hitsujiCounterProvider = StateProvider((ref) => 0);

  
  Widget build(BuildContext context, WidgetRef ref) {
        ...
    }
}

ConsumerStatefulWidget

⭐️State内からrefが参照できる。
⭐️Riverpod版のStatefulWidget。

sample
class MyHomePage extends ConsumerStatefulWidget {
  
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<MyHomePage> {
  final _hitsujiCounterProvider = StateProvider((ref) => 0);

  
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () =>
            ref.read(_hitsujiCounterProvider.notifier).update((state) => state + 1),
        child: const Icon(Icons.add),
      ),
    );
  }
}

~各Providerについて~

全部で6種類あり、管理する対象や更新方法に合わせて適切なPeoviderを選べる。
・Provider
・StateProvider
・StateNotifierProvider
・FutureProvider
・StreamProvider
・ChangeNotifierProvider

Provider

⭐️一度定義すると状態が外部から変更できない。(定数チック)
⭐️初期化時点で他のProviderの値を参照して、その値を加工した値をProviderの値として保持することも可能。
⭐️グローバルな値を提供するために使われることが多い。

https://riverpod.dev/ja/docs/providers/provider

定義

sample
final _hitsujiCounterProvider = ChangeNotifierProvider((ref) => HitsujiCounter());
late final _doubleProvider =
  Provider<int>((ref) => ref.watch(_hitsujiCounterProvider).hitsujiCounter * 2);

参照

sample
Text(
  '${ref.watch(_doubleProvider)}',
),

StateProvider

⭐️Providerのnotifierを参照し、直接値を変更する。(変数チック)
⭐️イミュータブルな状態を提供。
⭐️値の変更は新しい値を生成することによって行われる。

https://riverpod.dev/ja/docs/providers/state_provider
定義

sample
final animalTypeProvider =
    StateProvider<AnimalType>((ref) => AnimalType.neko);

参照

sample
final animalTypeNotifier = ref.watch(animalTypeProvider.notifier);

更新
・update : 今の値を加工した結果を設定したいときに使用する。
・state : 今の値が必要なときに使用する。

sample
// updateでの更新の例)
animalTypeNotifier.update((state) => AnimalType.inu);
// stateでの更新の例)
animalTypeNotifier.state = AnimalType.ushi;

StateNotifierProvider

⭐️状態を表す変数(state)と、状態を更新するメソッドを持つProvider。
⭐️StateNotifierを継承したクラスを監視して、状態に変化があれば画面を更新する。
⭐️高度な状態管理に向いている。
https://riverpod.dev/ja/docs/providers/state_notifier_provider
定義

sample
final animalTypeNotifierProvider = StateNotifierProvider<
    animalTypeNotifier, animalFoodTypeState>(
  (ref) => animalTypeNotifier(),
);

class animalTypeNotifier
    extends StateNotifier<animalFoodTypeState> {
  animalTypeNotifier()
      : super(const animalFoodTypeState());

  void checkFavoriteFoodType(AnimalFoodType value) {
    state = state.copyWith(animalFoodType: value);
  }
}

更新

sample
final animalFoodTypeStateNotifier =
        ref.watch(animalTypeNotifierProvider.notifier);

/// 呼び出し
animalFoodTypeStateNotifier
                        .checkFavoriteFoodType(animalFoodType);

参照

sample
final animalFoodTypeState = ref.watch(animalTypeNotifierProvider);

/// 呼び出し
Text(animalFoodTypeState.flavor);

ChangeNotifierProvider

⭐️ミュータブルなクラスの状態管理に使う。
⭐️Riverpodの前身のパッケージ「Provider」との互換のためにあるProviderで、パッケージの作者としては非推奨。
https://riverpod.dev/ja/docs/providers/change_notifier_provider

定義

sample
class HitsujiCounter extends ChangeNotifier {
  int _hitsujiCounter = 0;
  get hitsujiCounter => _hitsujiCounter;

  void hitsujiCountUp() {
    _hitsujiCounter++;
    notifyListeners();
  }
}
final _hitsujiCounterProvider = ChangeNotifierProvider((ref) => HitsujiCounter());

参照

sample
Text('${ref.watch(_hitsujiCounterProvider).hitsujiCounter}'),

更新

sample
ref.read(_hitsujiCounterProvider).hitsujiCountUp()

FutureProvider

⭐️Future型を取り扱うProvider。
⭐️非同期データの取得に使用して、データが使用可能(取得完了)になるとWidgetを更新する。
⭐️例としてAPIの呼び出しやSharedPreferencesなどの非同期処理など。
https://riverpod.dev/ja/docs/providers/future_provider

定義

sample
final animalListFetchProvider = FutureProvider((ref) async {
  return ref.watch(animalListRepositoryProvider).fetch();
});

参照

sample
final animalListAsync = ref.watch(animalListFetchProvider);
...
animalListAsync.when(
        data: (data) {
          final animalList = _createNewList(data);
          return animalList.isNotEmpty
              ? SingleChildScrollView(
                  child: Column(
                    children: [
                      for (final item in animalList) ...{
                        _Record(
                          item: item,
                        ),
                      },
                    ],
                  ),
                )
              : const NoDataDisplay();
        },
        error: (_, __) => const Text('error'),
        loading: () => loadingWidget,
      ),

更新(再取得)

sample
// 非同期データを取得する際に、キャッシュされたデータを破棄して新しいデータを再取得する
ref.invalidate(animalListFetchProvider)

StreamProvider

⭐️Future型を取り扱うProvider。
⭐️リアルタイムにデータを取得し、データがストリーム内で更新されるたびにウィジェットを更新する。
⭐️例としてWebSocketなど。
https://riverpod.dev/ja/docs/providers/stream_provider

定義

sample
final chatProvider = StreamProvider<List<String>>((ref) async* {
  final socket = await Socket.connect('my-api', 4242);
  ref.onDispose(socket.close);
  var allMessages = const <String>[];
  await for (final message in socket.map(utf8.decode)) {
    allMessages = [...allMessages, message];
    yield allMessages;
  }
});

参照

sample
final liveChats = ref.watch(chatProvider);
...
liveChats.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stackTrace) => Text(error.toString()),
    data: (messages) {
      return ListView.builder(
        reverse: true,
        itemCount: messages.length,
        itemBuilder: (context, index) {
          final message = messages[index];
          return Text(message);
        },
      );
    },
  );

値の参照

read

取得時点での状態を取得。
buildメソッド中では使わない。onTapなどのイベント内や、ライフサイクルのイベント(StatefulWidgetのinitStateなど)内で使用する。
https://zenn.dev/junq/articles/fa3dfd24a7ab84

watch

継続的に状態を監視する。buildメソッド中で使ってOK!
https://zenn.dev/junq/articles/73a9db44393f4a

listen

状態が変更された際に実行するイベントを定義できる。

sample
ref.listen(_stateProvider, (previous, next) {
  print('previous: $previous next: $next');
});

Provider修飾子

family

Providerに引数を渡せる。
https://riverpod.dev/ja/docs/concepts/modifiers/family

autoDispose

Providerが参照されなくなったタイミングで状態を破棄する。
https://riverpod.dev/ja/docs/concepts/modifiers/auto_dispose

ライフサイクルイベント

初期化のタイミング

Providerの定義時ではなく、ConsumerWidgetのbuild内などから実際に呼び出されたときに初期化される。

ref.onDispose

Providerが破棄されるときのイベントを指定する。

ref.onCancel

Providerの最後のリスナーが削除されたときに実行される。
(使ったことないかも。。。)

ref.onResume

Providerの最後のリスナーが削除された後、再度監視が開始されるときに実行される。
(これも使ったことない。。。)

sample
ref.onDispose(() => print('provider was disposed (page=$page)'));
ref.onCancel(() => print('provider was canceled (page=$page)'));
ref.onResume(() => print('provider was resumed (page=$page)'));

終わりに

riverpodをちゃんと理解できていなかったことを後ろめかしく思っていたので、今回色々調べてみて、過去に自分が書いたソースコード見てなんでここHooks使ってないのにHookConsumer使ってるんだろ🙄、、とか、ここで.autoDisposeしてる理由ってなんでだろ、、とか、メンバーが書いたものを見て「ふんふんなるほど」と思えるようになったりした気がします。

Discussion