💬

【Flutter】Provider.of(context)で「ProviderNotFoundException」が発生したときの対処

2023/09/30に公開

状態管理パッケージのProviderを使用する際に、切り出した関数でProvider.of(context, listen: false)を呼んで、ProviderNotFoundException(BuildContextの中にProviderが見つからない)というエラーを発生させたことが何回かあったので、備忘録を残す。

The following ProviderNotFoundException was thrown while handling a gesture:
Error: Could not find the correct Provider<MyStore> above this MyHomePage Widget

This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:

(最近はRiverpodが主流なのと、最新のProviderではcontext.read<T>()context.read<T>()があるので、この問題にあまり遭遇しないかもしれないけど。)
https://pub.dev/packages/provider

現象

【※Flutterのサンプルアプリで説明】

以下のコードのように_incrementCounter()関数の中で、ChangeNotifierのクラスのインスタンスを使うとき、Provider.of<T>()で呼び出す。

しかし、以下のように必要な引数のcontextは渡しているのに、「MyHomePageWidgetにProvider<Mytore>が見つかりません。そのBuildContextにProviderは含まれていません」というエラーが出力されてしまう。

class MyHomePage extends StatefulWidget {
  // 省略
}

class _MyHomePageState extends State<MyHomePage> {
  void _incrementCounter() {
      // ここでエラー↓
    final store = Provider.of<MyStore>(context, listen: false);
    store.addCounter();
  }

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyStore(counter: 0),
      builder: (BuildContext context, _) {
        return Scaffold(
	
          // 省略

          floatingActionButton: FloatingActionButton(
            onPressed: () => _incrementCounter(),
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      },
    );
  }
}

【エラーメッセージ】

The following ProviderNotFoundException was thrown while handling a gesture:
Error: Could not find the correct Provider<MyStore> above this MyHomePage Widget

This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:

- You added a new provider in your `main.dart` and performed a hot-reload.
  To fix, perform a hot-restart.

- The provider you are trying to read is in a different route.

  Providers are "scoped". So if you insert of provider inside a route, then
  other routes will not be able to access that provider.

- You used a `BuildContext` that is an ancestor of the provider you are trying to read.

  Make sure that MyHomePage is under your MultiProvider/Provider<MyStore>.
  This usually happens when you are creating a provider and trying to read it immediately.

  For example, instead of:

  ```
  Widget build(BuildContext context) {
    return Provider<Example>(
      create: (_) => Example(),
      // Will throw a ProviderNotFoundError, because `context` is associated
      // to the widget that is the parent of `Provider<Example>`
      child: Text(context.watch<Example>().toString()),
    );
  }
  ```

  consider using `builder` like so:

  ```
  Widget build(BuildContext context) {
    return Provider<Example>(
      create: (_) => Example(),
      // we use `builder` to obtain a new `BuildContext` that has access to the provider
      builder: (context, child) {
        // No longer throws
        return Text(context.watch<Example>().toString());
      }
    );
  }
  ```

原因と解消方法

エラーの原因は、Provider.of()に渡しているcontextが間違っているため。

以下のように、_incrementCounter()の引数でChangeNotifierProviderのbuilderプロパティのcontextを受け取るようにすれば、エラーは解消する。

class MyHomePage extends StatefulWidget {
  // 省略
}

class _MyHomePageState extends State<MyHomePage> {
  // BuildContextを受け取る↓
  void _incrementCounter(BuildContext context) {
    final store = Provider.of<MyStore>(context, listen: false);
    store.addCounter();
  }

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyStore(counter: 0),
      builder: (BuildContext context, _) { // ←ここのcontextを渡す
        return Scaffold(
	
          // 省略

          floatingActionButton: FloatingActionButton(
	      // ChangeNotifierProviderのbuildプロパティのcontextを渡す
            onPressed: () => _incrementCounter(context),
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      },
    );
  }
}

ofメソッドには正しいBuildContextを渡す

BuildContextは、親WidgetのElementを参照するデータを持っていて、親Widgetから子孫のWidgetの情報を持っている。

Provider.of()のofメソッドは、親Widgetから祖先のWidgetを辿る。

エラーの原因となっているProvider.of<T>()は、引数のcontextの親Widgetから祖先を辿って、生成したChangeNotifierのクラスのインスタンスを取得する。

なので、Provider.of<T>()contextを渡す場合は、そのcontextから祖先の間でChangeNotifierのインスタンスが生成されている必要がある。

しかし最初のコード↓だと、Provider.of<MyStore>(context, listen: false)に渡しているcontextは、framework.dartクラスで定義されている最上位のcontextなので、そこから祖先を辿っても目的のインスタンスは見つからない。

void _incrementCounter() {
    // 引数で渡しているのは、framework.dartで定義されている方のcontext
  final store = Provider.of<MyStore>(context, listen: false);
  store.addCounter();
}

それにより「ProviderNotFoundException」が発生する。

そのため、引数でChangeNotifierProviderbuilderプロパティのcontextを渡す必要がある。

class _MyHomePageState extends State<MyHomePage> {
  void _incrementCounter(BuildContext context) {
    // 渡したのは、ChangeNotifierProviderのcontext
    final store = Provider.of<MyStore>(context, listen: false);
    store.addCounter();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (BuildContext context) => MyStore(counter: 0),
      builder: (BuildContext context, _) { // ←ここのcontextを渡す
        return Scaffold(
	
          // 省略

          floatingActionButton: FloatingActionButton(
	      // ChangeNotifierProviderのbuildプロパティのcontextを渡す
            onPressed: () => _incrementCounter(context),
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      },
    );
  }
}

おわりに

contextとofメソッドの仕様を把握していれば、より理解が深まる問題だと思う。

Flutterの初学者はRiverpodではなくProviderから始めることもあるようなので、そういう方たちの参考になれれば。

Discussion