【Flutter】RiverpodでProviderScopeのoverrideをどう使うか

image

最近はFlutterの状態管理でRiverpodを使用しています。 その中でProviderScopeのoverrideが便利なので使い所を解説します。

この記事では、riverpod 1.0.0-dev.7を使用しています。

RiverpodのProviderFamilyについて

Riverpodを商品詳細のようなIDで内容が変わるページで使用した際、providerにIDを渡す必要があります。その際、よくあるパターンとしてProviderのFamilyを使用して以下の様に対応します。
detailPageProviderFamily(id)

特に以下のサンプルのような、詳細画面から詳細画面にpush遷移できるような場合では、Familyをほぼ必ず使用する必要があります。(Familyを使用しないと元の画面のproviderが引き継がれ、元の画面の状態にも影響を及ぼしてしまうため)

sample

View側

class DetailPage extends StatelessWidget {
  const DetailPage({Key? key, required this.id}) : super(key: key);

  final int id;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('詳細ページ')),
      body: Column(children: [
        Consumer(builder: (context, ref, child) {
          final name = ref.watch(detailPageProviderFamily(id).select((s) => s.name));
          return Text(name, style: TextStyle(fontSize: 30));
        }),
        Consumer(builder: (context, ref, child) {
          final description = ref.watch(detailPageProviderFamily(id).select((s) => s.description));
          return Text(description, style: TextStyle(fontSize: 14));
        }),
        Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
          for (var i = 1; i <= 5; i++)
            ElevatedButton(onPressed: () {
              Navigator.of(context).push(MaterialPageRoute(
                builder: (_) => DetailPage(id: i),
              ));
            }, child: Text('商品${i}'))
        ]),
        Consumer(builder: (context, ref, child) {
          return Container(
            margin: EdgeInsets.only(top: 100),
            width: 200,
            child: ElevatedButton(
              onPressed: ref.read(detailPageProviderFamily(id).notifier).onPurchase,
              child: Text('購入'),
            ),
          );
        }),
      ]),
    );
  }
}

Controller(Provider)側

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod/riverpod.dart';
import 'package:state_notifier/state_notifier.dart';

part 'detail_page_controller.freezed.dart';


class DetailPageState with _$DetailPageState {
  const factory DetailPageState({
    ('') String name,
    ('') String description,
  }) = _DetailPageState;
}

final detailPageProviderFamily = StateNotifierProvider.family
    .autoDispose<DetailPageController, DetailPageState, int>(
        (ref, id) {
  return DetailPageController(id: id);
});

class DetailPageController extends StateNotifier<DetailPageState> {
  DetailPageController({required int id})
      : _id = id,
        super(const DetailPageState()) {
    _init();
  }

  final int _id;

  Future<void> _init() async {
    state = state.copyWith(name: 'ID$_idの商品名', description: 'ID$_idの商品説明');
  }

  Future<void> onPurchase() async {
    // 商品の購入
  }
}

毎回IDを渡すのが大変

この実装をした時、Controller側を呼び出す度に、Providerに毎回IDを渡す必要が出てきて、非常に大変な実装となり不便です...

ProviderScopeのoverrideを使う

ここで、ProviderScopeのoverrideを使うと以下の様になります。

View側

class DetailPage extends StatelessWidget {
  const DetailPage({Key? key, required this.id}) : super(key: key);

  final int id;

  
  Widget build(BuildContext context) {
return ProviderScope(
overrides: [
detailPageProvider.overrideWithProvider(detailPageProviderFamily(id))
],
child: Scaffold( appBar: AppBar(title: Text('詳細ページ')), body: Column(children: [ Consumer(builder: (context, ref, child) {
final name = ref.watch(detailPageProvider.select((s) => s.name));
return Text(name, style: TextStyle(fontSize: 30)); }), Consumer(builder: (context, ref, child) {
final description = ref.watch(detailPageProvider.select((s) => s.description));
return Text(description, style: TextStyle(fontSize: 14)); }), Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ for (var i = 1; i <= 5; i++) ElevatedButton(onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (_) => DetailPage(id: i), )); }, child: Text('商品${i}')) ]), Consumer(builder: (context, ref, child) { return Container( margin: EdgeInsets.only(top: 100), width: 200, child: ElevatedButton(
onPressed: ref.read(detailPageProvider.notifier).onPurchase,
child: Text('購入'), ), ); }), ]), ), ); } }

Controller(Provider)側

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod/riverpod.dart';
import 'package:state_notifier/state_notifier.dart';

part 'detail_page_controller.freezed.dart';


class DetailPageState with _$DetailPageState {
  const factory DetailPageState({
    ('') String name,
    ('') String description,
  }) = _DetailPageState;
}

final detailPageProvider =
StateNotifierProvider.autoDispose<DetailPageController, DetailPageState>(
(ref) => throw UnimplementedError());
final detailPageProviderFamily = StateNotifierProvider.family .autoDispose<DetailPageController, DetailPageState, int>( (ref, id) { return DetailPageController(id: id); }); class DetailPageController extends StateNotifier<DetailPageState> { DetailPageController({required int id}) : _id = id, super(const DetailPageState()) { _init(); } final int _id; Future<void> _init() async { state = state.copyWith(name: 'ID$_idの商品名', description: 'ID$_idの商品説明'); } void onPurchase() { // 商品の購入 } }

呼び出しが簡単に

ProviderScopeのoverrideを使用することで、それ以下のWidgetではFamilyにIDを渡して呼び出す必要が無くなり、detailPageProviderの形で呼び出せるようになりました。

まとめ

Riverpodがバージョン1になり、進化した部分の一つとして、ProviderScopeが非常に便利になりました。
今回のサンプルではシンプルな画面なのでメリットが少なく感じますが、画面のコンテンツや状態やアクションが増えたとき、毎回IDを渡すというような実装は非常に苦しい実装になってしまいます。
Riverpodを使用する際はぜひこちらの記事を参考にしてみてください!