👏

[Flutter/Riverpod] ProviderScopeでoverrideしたのに上書きされない事象の解決

2022/05/30に公開

以前、RiverpodのProviderScopeでProviderを上書きしたはずなのにできていないという事態に直面しました。そのときの私の解決方法を記述しておきます。

上書きされない例

Riverpodを利用した簡単なカウンタープログラムです。クラスの引数でカウンターの初期値を受け取り、ProviderScopeのoverridesで上書きをしています。
しかし、以下のコードだとref.read(...).update(...)でUnimplementedErrorが投げられます。これはcounterProviderに初期値が上書きされていないからですね。

// 上書きするため、UnimplementedErrorを投げる
final counterProvider = StateProvider<int>((_) {
  throw UnimplementedError();
});

class CounterPage extends ConsumerWidget {
  const CounterPage({
    Key? key,
    required this.initValue,
  }) : super(key: key);
    
  // カウンターの初期値
  final int initValue;
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    return ProviderScope(
      overrides: [
        // Providerの値を上書きする
        counterProvider.overrideWithValue(StateController(initValue)),
      ],
      child: Scaffold(
        body: Center(
	  Consumer(
	    builder: (context, ref, _) {
	      // カウンターの値を取得する
	      final counter = ref.watch(counterProvider);
	    
	      // 値を表示
	      return Text('$counter');
	    },
	  ),
	),
	floatingActionButton: FloatingActionButton(
	  onPressed: () {
	    ref.read(counterProvider.notifier).update((state) => state + 1);  // UnimplementedError
	  },
	  child: const Icon(Icons.add),
	),
      ),
    );
  }
}

解決方法

refを使うWidgetの親にConsumerを挿入することで解決できます。

final counterProvider = StateProvider((_) {
  throw UnimplementedError();
});

class CounterPage extends ConsumerWidget {
  const CounterPage({
    Key? key,
    required this.initValue,
  }) : super(key: key);
    
  final int initValue;
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    return ProviderScope(
      overrides: [
	counterProvider.overrideWithValue(StateController(initValue)),
      ],
      child: Scaffold(
        body: Center(
	  Consumer(
	    builder: (context, ref, _) {
	      final counter = ref.watch(counterProvider);
	      return Text('$counter');
	    },
	  ),
	),
	// Consumerを追加する
	floatingActionButton: Consumer(
	  builder: (context, ref, _) {
	    FloatingActionButton(
	      onPressed: () {
	        // Consumerのrefを使用することで上書き後のProviderを参照できる
	        ref.read(counterProvider.notifier).update((state) => state + 1);
	      },
	      child: const Icon(Icons.add),
	    ),
	  },
	),
      ),
    );
  }
}

Consumerのrefを使うことで、上書き後のProviderを参照することができます。上書きされない例では、ProviderScopeで上書きされる前のConsumerWidgetのrefを使用していたので、上書きされる前のProviderを参照していたのですね。

別の書き方として、ProviderScopeの子にConsumerを置いても大丈夫です。

ProviderScope(
  overrides: [
    counterProvider.overrideWithValue(StateController(initValue)),
  ],
  child: Consumer(
    builder: (context, ref, _) {
      return Scaffold(
        body: ,                  // 以下略
	floatingActionButton: ,  // 以下略
      );
    },
  ),
),

私はこの落とし穴から抜け出すのに意外と時間を使ってしまいました...
overrideする場合は気を付けましょう。

Flutter勉強中の身です。何か間違いがありましたらコメントいただけると幸いです。

Discussion