34

I have a problem with provider and navigation.

I have a HomeScreen with a list of objects. When you click on one object I navigate to a DetailScreen with tab navigation. This DetailScreen is wrapped with a ChangenotifierProvider which provides a ViewModel

Now, when I navigate to another screen with Navigator.of(context).push(EditScreen) I can't access the ViewModel within the EditScreen The following error is thrown

════════ Exception caught by gesture ═══════════════════════════════════════════
The following ProviderNotFoundException was thrown while handling a gesture:
Error: Could not find the correct Provider<ViewModel> above this EditScreen Widget

This is a simple overview of what I try to achieve

Home Screen
 - Detail Screen (wrapped with ChangeNotifierProvider)
   - Edit Screen
     - access provider from here

I know what the problem is. I'm pushing a new screen on the stack and the change notifier is not available anymore. I thought about creating a Detail Repository on top of my App which holds all of the ViewModels for the DetailView.

I know I could wrap the ChangeNotifier around my MaterialApp, but I don't want that, or can't do it because I don't know which Detail-ViewModel I need. I want a ViewModel for every item in the list

I really don't know what's the best way to solve this. Thanks everyone for the help

Here is a quick example app:

This is a picture of the image tree

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: RaisedButton(
      child: Text("DetailView"),
      onPressed: () => Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => ChangeNotifierProvider(
              create: (_) => ViewModel(), child: DetailScreen()))),
    )));
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text("EditScreen"),
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => EditScreen())),
      ),
    ));
  }
}

class EditScreen extends StatelessWidget {
  const EditScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
            child: Text("Print"),
            onPressed: () =>
                Provider.of<ViewModel>(context, listen: false).printNumber()),
      ),
    );
  }
}

class ViewModel extends ChangeNotifier {
  printNumber() {
    print(2);
  }
}
Josef Wilhelm
  • 361
  • 1
  • 3
  • 6

3 Answers3

24

To be able to access providers accross navigations, you need to provide it before MaterialApp as follows

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ViewModel(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: RaisedButton(
      child: Text("DetailView"),
      onPressed: () => Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => DetailScreen(),
        ),
      ),
    )));
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text("EditScreen"),
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => EditScreen())),
      ),
    ));
  }
}

class EditScreen extends StatelessWidget {
  const EditScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
            child: Text("Print"),
            onPressed: () =>
                Provider.of<ViewModel>(context, listen: false).printNumber()),
      ),
    );
  }
}

class ViewModel extends ChangeNotifier {
  printNumber() {
    print(2);
  }
}
dlohani
  • 2,511
  • 16
  • 21
  • 4
    Thanks for your answer. I know that this works, but I can't do that because I don't only have one ViewModel but one ViewModel for every DetailScreen. – Josef Wilhelm Jan 03 '20 at 13:00
  • I think then, you need to provide each ViewModel using multi provider, If they are all different classes – dlohani Jan 03 '20 at 14:54
  • I don't understand. I need to inialise the View models with data from the list. so I cant provide them 'above' my main app – Josef Wilhelm Jan 04 '20 at 01:02
  • If you are passing instance of viewmodel to each detail page on list click, why bother using provider, just pass the instance directly as parameters to widgets, why not do that? – dlohani Jan 04 '20 at 01:41
  • it's explained in my original question. viewmodel is maybe the wrong word, let's say bloc instead. I want to use in the edit view that I push. I hope this is understandable. I don't really understand what you mean with passing into the detail page? so I should pass it into the detail page and in every widget that I use in that page? plus the edit screen? – Josef Wilhelm Jan 04 '20 at 10:30
  • Checkout this tutorial, this might be helpful in your case https://medium.com/flutter-community/flutter-architecture-provider-implementation-guide-d33133a9a4e8 – dlohani Jan 04 '20 at 10:45
  • Also, to answer your question, check out this issue, reply from the provider creator https://github.com/rrousselGit/provider/issues/128 – dlohani Jan 04 '20 at 10:47
  • 4
    thanks so much for your help. I know this tutorial, but it doesn't help in my case, same problem if I want to do it the way I mentioned in my question. And Remi's answer just mentioned that I need to include the provider in my route, but not how it is done. that is actually my question :/ I know I could design my app a little bit differentely but every other way doesn't feel natural to me, therefore my question – Josef Wilhelm Jan 05 '20 at 11:16
  • 4
    Did you find an answer to this question @JosefWilhelm? – anonymous-dev May 13 '20 at 11:19
  • To be able to access providers accross navigations, you need to provide it before MaterialApp as follows – dlohani May 14 '20 at 00:52
  • 4
    I have same problem with @JosefWilhelm. Put provider/multi provider before MaterialApp is not what we want, even though it will works. – fajar ainul Jun 09 '20 at 04:11
  • 2
    try new [riverpod](https://pub.dev/packages/riverpod) state management from the same developer, complete rewrite of provider – dlohani Jul 06 '20 at 16:22
18

A bit late to the party, but I think this is the answer the question was looking for:

(Basically passing the ViewModel down to the next Navigator page.)

class DetailScreen extends StatelessWidget {
  const DetailScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<ViewModel>(context); // Get current ViewModel
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text("EditScreen"),
        onPressed: () => Navigator.of(context).push(
          // Pass ViewModel down to EditScreen
          MaterialPageRoute(builder: (context) {
            return ChangeNotifierProvider.value(value: viewModel, child: EditScreen());
          }),
        ),
      ),
    ));
  }
}

Nicolas Degen
  • 1,522
  • 2
  • 15
  • 24
  • 2
    This answer is more a proper solution than the others. The provider above MaterialApp means the provider will always be "alive" consuming resources. That's not what you usually want. Following this answer, for those pushing a named route, you can pass the provider as an argument and call `ChangeNotifierProvider.value()` in your destination screen. Not nice the need to pass it as an argument but I haven't found any other way that looks more proper than this solution. – maganap May 28 '22 at 12:16
3

I am a bit late but I found a solution on how to keep the value of a Provider alive after a Navigator.push() without having to put the Provider above the MaterialApp.

To do so, I have used the library custom_navigator. It allows you to create a Navigator wherever you want in the tree.

You will have to create 2 different GlobalKey<NavigatorState> that you will give to the MaterialApp and CustomNavigator widgets. These keys will allow you to control what Navigator you want to use.

Here is a small snippet to illustrate how to do

class App extends StatelessWidget {

   GlobalKey<NavigatorState> _mainNavigatorKey = GlobalKey<NavigatorState>(); // You need to create this key for the MaterialApp too

   @override
   Widget build(BuildContext context) {
      return MaterialApp(
         navigatorKey: _mainNavigatorKey;  // Give the main key to the MaterialApp
         home: Provider<bool>.value(
            value: myProviderFunction(),
            child: Home(),
         ),
      );
   }

}

class Home extends StatelessWidget {

   GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); // You need to create this key to control what navigator you want to use

   @override
   Widget build(BuildContext context) {

      final bool myBool = Provider.of<bool>(context);

      return CustomNavigator (
         // CustomNavigator is from the library 'custom_navigator'
         navigatorKey: _navigatorKey,  // Give the second key to your CustomNavigator
         pageRoute: PageRoutes.materialPageRoute,
         home: Scaffold(
            body: FlatButton(
               child: Text('Push'),
               onPressed: () {
                  _navigatorKey.currentState.push(  // <- Where the magic happens
                     MaterialPageRoute(
                        builder: (context) => SecondHome(),
                     ),
                  },
               ),
            ),
         ),
      );   
   }
}

class SecondHome extends StatelessWidget {

   @override
   Widget build(BuildContext context) {

      final bool myBool = Provider.of<bool>(context);

      return Scaffold(
         body: FlatButton(
            child: Text('Pop'),
            onPressed: () {
               Novigator.pop(context);
            },
         ),
      );
   }

}

Here you can read the value myBool from the Provider in the Home widget but also ine the SecondHome widget even after a Navigator.push().

However, the Android back button will trigger a Navigator.pop() from the Navigator of the MaterialApp. If you want to use the CustomNavigator's one, you can do this:

// In the Home Widget insert this
   ...
   @override
   Widget build(BuildContext context) {
      return WillPopScope(
         onWillPop: () async {
            if (_navigatorKey.currentState.canPop()) {
               _navigatorKey.currentState.pop();  // Use the custom navigator when available
               return false;  // Don't pop the main navigator
            } else {
               return true;  // There is nothing to pop in the custom navigator anymore, use the main one
            }
         },
         child: CustomNavigator(...),
      );
   }
   ...
Dharman
  • 30,962
  • 25
  • 85
  • 135
Valentin Vignal
  • 6,151
  • 2
  • 33
  • 73