Josef Wilhelm
Josef Wilhelm

Reputation: 391

Flutter Provider nested navigation

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);
  }
}

Upvotes: 37

Views: 28325

Answers (3)

Nicolas Degen
Nicolas Degen

Reputation: 1846

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());
          }),
        ),
      ),
    ));
  }
}

Upvotes: 23

Valentin Vignal
Valentin Vignal

Reputation: 8172

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(...),
      );
   }
   ...

Upvotes: 4

dlohani
dlohani

Reputation: 2591

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);
  }
}

Upvotes: 28

Related Questions