31

The Provider package makes use of InheritedWidget. This is a problem when I want to access a provider when I'm in a Dialog. If I load a dialog using

 showDialog(... builder: (context) => MyDialog);

I can't access anything using InheritedWidget because my dialog isn't part of the main widget tree. This also means that I can't access my Provider providers, correct?

My question is: How can I access my providers in a dialog if it's not part of the main app widget tree?

final firebaseAuth = Provider.of<FirebaseAuth>(context);

I have the same problem with using BLoCs. If I try to retrieve them in a dialog via InheritedWidget, they fail. I've gotten around this by passing the BLoC in the constructor but this seems to defeat the purpose of InheritedWidgets.

Samet ÖZTOPRAK
  • 3,112
  • 3
  • 32
  • 33
JustLearningAgain
  • 2,227
  • 4
  • 34
  • 50
  • Not the solution but a suggestion: Using BLoC Pattern it's very common to inject the BLoC into your component using dependency injection. In this case you don't rely on the widget tree to receive the instance. Personally I recommend `getIt` for DI. – Philip Feldmann Sep 17 '19 at 07:31
  • I have tried that and I like it. But I've seen where the Provider package does almost everything you need. The only problem is this whole widget tree thing. I'm hoping there is a slick way of using "Provider" throughout my app and NOT have to resort to getIt. But I agree, this is definitely a solution. – JustLearningAgain Sep 17 '19 at 10:47

10 Answers10

19

Instead of passing the BLoC in the constructor, you can make use of BlocProvider.value.

https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocProvider/BlocProvider.value.html

This will allow you to provide your existing BLoC instance to your new route (the dialog). And you still get all the benefits of InheritedWidget

  // Get the BLoC using the provider
  MyBloc myBloc = BlocProvider.of<MyBloc>(context);

  showDialog(
    context: context,
    builder: (BuildContext context) {
      Widget dialog = SimpleDialog(
        children: <Widget>[
          ... // Now you can call BlocProvider.of<MyBloc>(context); and it will work
        ],
      );

      // Provide the existing BLoC instance to the new route (the dialog)
      return BlocProvider<MyBloc>.value(
        value: myBloc, //
        child: dialog,
      );
    },
  );

.value() also exists for ChangeNotifierProvider, ListenableProvider, etc. https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider/ChangeNotifierProvider.value.html

https://pub.dev/documentation/provider/latest/provider/ListenableProvider/ListenableProvider.value.html

SteveM
  • 722
  • 1
  • 6
  • 12
14

I got stuck at this part for a while. I honestly didn't want to pass the provider, also unpacking the widget code to grab the parent context is hard when you are dealing with a complex widget (And it doesn't seem like the best approach).

This made more sense

  handleFileViewerClicked(context) async {
    var reportState = Provider.of<ReportState>(context, listen: false);
    /**
     *The dialog will live in a new context and requires a new provider to be created for the report state
     * For more information read the Provider.Consumer documentation and showDialog function signature.
     */
    showDialog(
      context: context,
      //Notice the use of ChangeNotifierProvider<ReportState>.value
      builder: (_) => ChangeNotifierProvider<ReportState>.value(
        value: reportState,
        child: FileViewer(),
      ),
    );
}

Your child widget which is FileViewer in that case can make use of

class FileViewer extends StatelessWidget {
.
.
Widget build(BuildContext context) {
    //you can enable or disable listen if you logic require so 
    var reportState = Provider.of<ReportState>(context); 
    return Text('${reportState.files.length}');
 }
}
Kareem
  • 819
  • 9
  • 11
1

I was able to access Provider data by passing in the data set into the alert dialog. Interestingly, you have to call setState() in the Dialog in order to see the changes in your Dialog.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {

    final provider = Provider.of<DataSet>(context);

    return Scaffold(
      body: Container(
        child: RaisedButton(
        child: Text('Show Dialog'),
          onPressed: () {
            showDialog(context: context,
            builder: (context) {
              return DialogContent(dataSet: provider);
            });
          },
        ),
      ),
    );
  }
}

class DialogContent extends StatefulWidget {

  final DataSet dataSet;

  const DialogContent({Key key, this.dataSet}) : super(key: key);

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

class _DialogContentState extends State<DialogContent> {
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Dialog with data'),
      content: Text('${widget.dataSet.pieceOfData}'),
      actions: <Widget>[
        FlatButton(
          child: Text('Increase Data'),
          onPressed: () {
            setState(() {
              widget.dataSet.increaseData();
            });
          },
        ),
      ],
    );
  }
}

class DataSet with ChangeNotifier {
  int pieceOfData = 1;

  increaseData() {
    pieceOfData += 1;
    notifyListeners();
  }
}
Eric Duffett
  • 1,654
  • 17
  • 25
  • Don't you get an error when you're calling Provider.of(context)? I don't see where you are using the ChangeNotifierProvider... – Gpack Feb 23 '20 at 04:49
  • @Gpack Sorry for not including that in my post. I've found that, most of the time, the Provider package works best when I implement it at the highest level possible. So, I return ChangeNotifierProvider as the first Widget in the build method of MyApp – Eric Duffett Feb 23 '20 at 13:57
  • void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return ChangeNotifierProvider( builder: (context) => BabyInfo(), child: MaterialApp( debugShowCheckedModeBanner: false, initialRoute: '/', routes: { '/': (context) => MyHomePage(), '/settings': (context) => CalorieSettings(), }, theme: ThemeData( primarySwatch: Colors.lightBlue, ), ), ); } } – Eric Duffett Feb 23 '20 at 13:57
1

Try this. Create a different stateful widget that housed the dialog and return that dialog stateful widget when you call a showDialog() method. Example below

class MainScreen extends StatefulWidget {
  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {

  @override
  void initState() {
    super.initState();

  }

  @override
  void dispose() {
    super.dispose();
  }   

  @override
  Widget build((BuildContext context) {
    MainProvider mainProvider = MainProvider.of(context);

    return Scaffold(
        appBar: AppBar(
            elevation: 0,
            backgroundColor: Colors.white,
        ),
        body: Center(
            child: Container(
                child: RaisedButton(
                    onPressed: ()=> _openBottomSheet(context, mainProvider),
                    child: Text("Open Dialog"),
                )
            )
        )
    );
}

_openBottomSheet(BuildContext context, MainProvider mainProvider) async {
    await showModalBottomSheet<bool>(
        context: cntxt,
        builder: (_) {
            return BottomSheetDialog();
        }
    );
}

}

class BottomSheetDialog extends StatefulWidget {
  @override
  _BottomSheetDialogState createState() => _BottomSheetDialogState();
}

class _BottomSheetDialogState extends State<BottomSheetDialog> {

   @override
   void initState() {
     super.initState();

   }

   @override
   void dispose() {
    super.dispose();
   }

   @override
   Widget build(BuildContext context) {
      MainProvider mainProvider = MainProvider.of(context);

      return Container(
        width: MediaQuery.of(context).size.width,
        height:MediaQuery.of(context).size.height/2.2,
        margin: EdgeInsets.fromLTRB(16,16,16,0),
        decoration: BoxDecoration(
            color: mainProvider.color,
            borderRadius: BorderRadius.only(
                topLeft: Radius.circular(20),
                topRight: Radius.circular(20),
            ),
        ),
        child: RaisedButton(    
            onPressed: ()=> mainProvider.changeColor(),
            child: Text("Open Dialog"),
        )
    )

}

}


class MainProvider with ChangeNotifier {

  static MainProvider of(BuildContext context) {
    return Provider.of<MainProvider>(context);
  }

  Color _color = Colors.white;
  bool _isColorChanged = false;

  Color get color => _color;
  bool get isColorChanged => _isColorChanged;

  changeColor() {
    if(!isColorChanged) {
        _color = Colors.green;
    }else{
        _color = Colors.white;
    }
    _isColorChanged = !_isColorChanged;

    notifyListeners();
  }


}
Iredia Ebikade
  • 1,785
  • 14
  • 18
1

If that's an option for you, simply lift the provider up above MaterialApp. This might be a good solution for globally unique providers, e.g. user configurations or similar: enter image description here

rgisi
  • 858
  • 6
  • 21
0

You have to pass the thing being provided directly to the dialog constructor to access it in the dialog's new context. You can also give it to a new Provider widget at the top of your dialog tree if you have a very deep widget tree in the dialog and you want to access it from somewhere deeper.

If you are using Bloc, typically you tell Provider to call the Bloc's dispose method when the provider widget is disposed to clean up the streamcontrollers/subscriptions. Obviously, you might not want to do this if you are re-providing the bloc to the dialog, or if this bloc is used outside the dialog.

Using stateful or stateless widgets in the dialog is up to you, as long as you have access to the bloc you can use a streambuilder and listen to some stream as per usual.

an example:

class EditEventDialog extends StatelessWidget {

  final GroupBloc groupBloc;

  EditEventDialog({this.groupBloc})
      : assert(groupBloc != null);

  @override
  Widget build(BuildContext context) {
    return Provider(
      builder: (context) => groupBloc,
      child: Dialog(
        child: Container(
          height: 400.0,
          width: 200.0,
          child: StreamBuilder<StatusStreamType>(
            stream: groupBloc.statusStream,
            builder: (context, snapshot) {
    ....

and to call it:

onPressed: () => showDialog(
                    builder: (newContext) {
                      GroupBloc groupBloc = Provider.of<GroupBloc>(context);
                      return EditEventDialog(
                        groupBloc: groupBloc,
                      );
                    },
                    context: context,
                  )
Kris
  • 3,091
  • 20
  • 28
0

I faced the same issue today and I was able to work around it by wrapping the dialog in a Stateful Builder and setting the state in the new widget tree.

      context: context,
      builder: (context) {
        return StatefulBuilder(builder: (context, setState) {
          return Dialog(
            child: SingleChildScrollView(
              child: Container(
                child: SingleChildScrollView(
                  child: Column(
                    children: <Widget>[
                      Padding(
                        padding: EdgeInsets.symmetric(vertical: height * .05),
                        child: Text('Choose An Avatar'),
                      ),
                      Stack(
                        children: <Widget>[
                          Align(
                            alignment: Alignment.center,
                            child: CircleAvatar(
                              minRadius: width * .09,
                              maxRadius: width * .09,
                              backgroundColor: Colors.brown,
                              backgroundImage: AssetImage(
                                  'assets/profile${appData.avatar}.png'),
                            ),
                          ),
                          Positioned.fill(
                            left: width * .04,
                            child: Align(
                              alignment: Alignment.centerLeft,
                              child: Container(
                                width: width * .18,
                                child: Material(
                                  color: Colors.transparent,
                                  child: InkWell(
                                    child: Icon(Icons.arrow_left,
                                        size: width * .18),
                                    onTap: () {
                                      setState(() {
                                        appData.changeAvatar();
                                      });
                                    },
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        });
      });
0

I only way I've found to gain access to the Bloc provider from within the dialog is by defining the dialog outside of the showDialog call.

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<MyCubit, MyState>(
      listener: (context, state) {
        if (state.shouldShowDialog == true) {
          final dialog = AlertDialog(
            content: Text("Info");
            actions: <Widget>[
              TextButton(
                child: const Text('Approve'),
                onPressed: () => {
                  context
                    .read<MyCubit>()
                    .handleDialogApproved();
                  Navigator.of(context, rootNavigator: true).pop();
                }
              )
            ],
          );

          showDialog<void>(
            context: context,
            builder: (BuildContext context) {
              return dialog;
            },
          );
        }
      },
      builder: (context, state) {
        return Container();
      },
    );
  }
}
Abtin Gramian
  • 1,630
  • 14
  • 13
0
Widget reviseRatesButton(BuildContext c) {
return Consumer<RideRequestProvider>(
  builder: (c, provider, child) {
    return OutlinedButton(
      onPressed: () async {
        alertDialogNew(
          c,
          content: ChangeNotifierProvider.value(
              value: provider,
              builder: (context, child) {
                return Consumer<RideRequestProvider>(
                  builder: (context, provider, child) {
                    return Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        children: [
                          const Text(
                            "Offer your fare",
                            textAlign: TextAlign.center,
                            style: TextStyle(
                              fontWeight: FontWeight.w600,
                              fontSize: 16,
                            ),
                          ),
                          const SizedBox(
                            height: 5,
                          ),
                          CustomTextFormField(
                            hint: "Enter your fair/day",
                            keyboardType: TextInputType.number,
                            controller: provider.fareController,
                            onChanged: (String? val) {
                              provider.calculateFare();
                            },
                          ),
                          const SizedBox(
                            height: 5,
                          ),
                          Row(
                            mainAxisAlignment:
                                MainAxisAlignment.spaceEvenly,
                            children: [
                              Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  const Text(
                                    'Weekly (5 days)',
                                    style: TextStyle(
                                      fontSize: 14,
                                      fontWeight: FontWeight.w500,
                                    ),
                                  ),
                                  Text.rich(
                                    TextSpan(
                                      text: provider.weeklyFare
                                          .toStringAsFixed(2),
                                      children: [
                                        TextSpan(
                                          text: '/week',
                                          style: TextStyle(
                                            color: Colors.blue.shade700,
                                            fontSize: 12,
                                            fontWeight: FontWeight.w600,
                                          ),
                                        ),
                                      ],
                                    ),
                                    style: const TextStyle(
                                      fontSize: 16,
                                      fontWeight: FontWeight.w600,
                                    ),
                                  ),
                                ],
                              ),
                              Column(
                                children: [
                                  const Text(
                                    'Monthly(22 days)',
                                    style: TextStyle(
                                      fontSize: 14,
                                      fontWeight: FontWeight.w500,
                                    ),
                                  ),
                                  Text.rich(
                                    TextSpan(
                                      text: provider.monthlyFare
                                          .toStringAsFixed(2),
                                      children: [
                                        TextSpan(
                                          text: '/month',
                                          style: TextStyle(
                                            fontSize: 12,
                                            color: Colors.blue.shade700,
                                            fontWeight: FontWeight.w600,
                                          ),
                                        ),
                                      ],
                                    ),
                                    style: const TextStyle(
                                      fontSize: 16,
                                      fontWeight: FontWeight.w600,
                                    ),
                                  ),
                                ],
                              ),
                            ],
                          ),
                        ],
                      ),
                    );
                  },
                );
              }),
        );
      },
      child: const Text(
        "Revise Rates",
      ),
      style: OutlinedButton.styleFrom(
        side: const BorderSide(width: 1.0, color: Colors.blue),
      ),
    );
  },
);}

I've been stuck at this for a few moments, but ChangeNotifierProvider.value works like a charm.

C.M Talha
  • 373
  • 2
  • 6
-1

A bit late in finding this, but just had this same challenge and realised a solution: You need to maintain a reference to the context outside of the showDialog call. By default we usually just use "context" as the name of the context both outside and inside the showDialog, thus masking the outside context from use within the showDialog. So, instead, use a different name inside the showDialog (e.g. "c") and then you can still use "final firebaseAuth = Provider.of(context);" inside the showDialog and it will find the FirebaseAuth object from the main tree as you wish.

Here's a short excerpt from some code I am working on which works now:

showDialog(
    context: context,
    builder: (c) {
        final action = Provider.of<ActionType>(context);
        final host = Provider.of<String>(context);
        return AlertDialog(
            title: Text('Action API'),
            actions: [
                FlatButton(
                    onPressed: () {
                        Navigator.pop(c);
                    },

etc.