Flutter: can't access Provider from showModalBottomSheet

3k Views Asked by At

I can't access a provider defined above a Scaffold from showModalBottomSheet in the FloatingActionButton.

I've defined a HomePage like so:

class HomePage extends StatelessWidget {
 @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyProvider(),
      builder: (context, _) {
        return Scaffold(
          body: Consumer<MyProvider>(
             builder: (context, provider, _) {
               return Text(provider.mytext); // this works fine
             }
          ),
          floatingActionButton: MyFAB(), // here is the problem
        );
      }
    )
  }
}

And this is MyFAB:

class MyFAB extends StatefulWidget {
  @override
  _MyFABState createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      ...
      onPressed: () => show(),
    );
  }
  
  void show() {
    showModalBottomSheet(
      ...
      context: context,
      builder: (BuildContext context) {
        return Wrap(
          children: [
            ...
            FlatButton(
              onPressed: Provider.of<MyProvider>(context, listen: false).doSomething(); //Can't do this
              Navigator.pop(context);
            )
          ],
        );
      }
    );
  }
}

Error: Could not find the correct Provider<MyProvider above this BottomSheet Widget.

4

There are 4 best solutions below

0
On

SOLUTION

HomePage:

class HomePage extends StatelessWidget {
 @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyProvider(),
      builder: (context, _) {
        return Scaffold(
          body: Consumer<MyProvider>(
             builder: (context, provider, _) {
               return Text(provider.mytext); // this works fine
             }
          ),
          floatingActionButton: MyFAB(context), // here is the problem
        );
      }
    )
  }
}

MyFAB:

class MyFAB extends StatefulWidget {
final BuildContext ctx;

MyFAB(this.ctx)

  @override
  _MyFABState createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      ...
      onPressed: () => show(),
    );
  }
  
  void show() {
    showModalBottomSheet(
      ...
      context: context,
      builder: (BuildContext context) {
        return Wrap(
          children: [
            ...
            FlatButton(
              onPressed: Provider.of<MyProvider>(widget.ctx, listen: false).doSomething(); //Can't do this
              Navigator.pop(context);
            )
          ],
        );
      }
    );
  }
}
5
On

This is caused by passing it the wrong context. Wrap your FAB to a Builder widget and pass it as builder property. This will take a new context and pass it to showModalBottomSheet. Also, you can do onPressed: show, it's more concise.

 class HomePage extends StatelessWidget {
 @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyProvider(),
      builder: (context, _) {
        return Scaffold(
          body: Consumer<MyProvider>(
             builder: (context, provider, _) {
               return Text(provider.mytext); // this works fine
             }
          ),
          floatingActionButton: MyFAB(context), // here is the problem
        );
      }
    )
  }
}


class MyFAB extends StatefulWidget {
  @override
  _MyFABState createState() => _MyFABState();
}

class _MyFABState extends State<MyFAB> {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      ...
      onPressed: (context) => show(context),
    );
  }
  
  void show(ctx) {
    showModalBottomSheet(
      ...
      context: ctx,
      builder: (BuildContext context) {
        return Wrap(
          children: [
            ...
            FlatButton(
              onPressed: () {
              Provider.of<MyProvider>(ctx, listen: false).doSomething(); //Can't do this
              Navigator.pop(ctx)
              };
            )
          ],
        );
      }
    );
  }
}
0
On

Fixed by placing the provider above MaterialApp, as described here.

Bottom sheets are created at the root of the material app. If a prodiver is declared below the material app, a bottom sheet cannot access it because the provider is not an ancestor of the bottom sheet in the widget tree.

The screenshot below shows a widget tree: the whole app is inside Wrapper and the bottom sheet is not created inside Wrapper. It is created as another child of MaterialApp (with a root element Container in this case).

widget tree

For your case:

// main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyProvider(),
      builder: (context, _) {
        return MaterialApp(
          home: HomePage(),
        );
      },
    );
  }
}
// home_page.dart
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: MyFAB()
    );
  }
}
0
On

In my opinion: showModalBottomSheet builds a bottom sheet with context which comes from Material App 1st image so when we return any Widget to show in the Bottom sheet it uses that Material app context as we can see in the builder property in the:1st image.

2ng Image: your code so in your code, when you are writing: Provider.of(context, listen: false).doSomething(); it is using context from the builder: (BuildContext context) which is the context of Material App. we have to change this context in order to use this Provider without having to uplift the position of our Provider above the Material App.

Now if we want to keep using that context to get the benefits of that overlay and automatic detection of suitable themes and still want to use the context of a widget that does have access to our provider:

we can pass the context of the Widget which does have Provider access to the FAB, but we will have to keep passing that context through widgets till we need to use that Provider in our FAB or till we go to a different route: in which case we can start from a new context and provider as Providers are scoped in mature.

so in your HomePage either you can wrap your scaffold inside a Builder or you can create a new widget like this:" 3rd image

so that it will have its own context which does have access to the provider we need inside our FAB as shown below in 4th image: 4th image

and then in the builder property of showModalBottomSheet change the name of the parameter in an anonymous function so that it won't be confused with the MAterial App context and context we will be passing in (Builder context or IdeaScreen context in my case image 4th)

5th image

I am creating a new widget but you do not have need to do so you can directly write your Fab code inside the anonymous function: and can use context(not newContext which is related to Material App context) while calling the Provider as you are already doing. But I will show in my case What I am doing in my AddTask Widget in case anyone's use case is similar to mine:

6th image expect a context, which does have a provider access, I my case its context of IdeaScreen.

and then use it just like this: 7th image