Flutter access the Notifier on a push page with provider

Viewed 516

APP DESCRIPTION

My application consists of 3 main pages accessible in my BottomNavigationBar. When I navigate between the menus I want my pages to be initialized on each call.

In my Menu1 with my form, I have the possibility to submit my form. This opens a page with a pushNamed that we will call SubMenu1. This page displays results calculated based on the form data. On this page I have the possibility to go back with pop and do nothing, or I have a button which must update me certain values ​​of my form.

PROBLEM

How to update the values ​​of the Menu1 form from SubMenu1? Indeed, when you push a page, it ends up at MaterialApp level and therefore does not have access to my Notifier from Menu1. The solution is to pass the Notifier above MaterialApp then. But suddenly, my form is no longer initialized each time Menu1 is called, but only once when the application starts ...

CODE

In the code provided, I placed the notify above MaterialApp. So I have access to my Notifier in SubMenu1 and I can modify the values ​​of the fields in Menu1, but when I change the page from my navigation bar and return the values ​​are still present, the page has not initialized .

AppScreen :

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

  @override
  Widget build(BuildContext context) {

    return ChangeNotifierProvider<LicenseNotifier>(
      create: (_) => LicenseNotifier(),
      child: ChangeNotifierProvider<BottomNavigationBarNotifier>( // Here my navigation 
        create: (BuildContext context) => BottomNavigationBarNotifier(),
        child: ChangeNotifierProvider<Menu1Notifier>( // Here my form Notifier for Menu 1
          create: (BuildContext context) => Menu1Notifier(),
          child: MaterialApp(
            title: AppConfig.APPLICATION_NAME,
            debugShowCheckedModeBanner: false,
            theme: AppTheme().data,
            initialRoute: AppRoutes.HOME,
            onGenerateRoute: RoutesClass.generate,
          ),
        )
      )
    );
  }
}

My BottomNavigationBarNotifier :

class BottomNavigationBarNotifier with ChangeNotifier {

  int currentIndex = AppConfig.NAVIGATION_DEFAULT_INDEX;

  BottomNavigationBarNotifier();

  Future<void> navigationScreenIndex({int index}) async{
    currentIndex = index;
    notifyListeners();
  }

  Widget loadScreenWithNavigation()
  {
    switch (currentIndex)
    {
      case 0:
        return Menu1Screen(title: 'Menu 1');
        break;

      case 1:
        return Menu2Screen(title: 'Menu 2');
        break;

      case 2:
        return Menu3Screen(title: 'Menu 3');
        break;

      default:
        return Menu1Screen(title: 'Menu 1');
        break;
    }
  }
}

My notifier for Menu1Screen :

class Menu1Notifier extends FormNotifier {

  TextEditingController controllerTest;
  
  Menu1Notifier (){
    _initialise();
  }

  Future _initialise() async{
    controllerTest = TextEditingController();
    notifyListeners();
  }
}
1 Answers

I am not sure I understand exactly what you are doing from the code fragments above, but I think I understand the requirement:

  • Menu1 has state
  • SubMenu1 should be able to read and update the Menu1 state
  • Menu1 invokes SubMenu1 as a "child" screen, there is no other path to SubMenu1

The simplest way to handle such simple state is for Menu1 to pass its state to SubMenu1 (as a constructor argument) when it displays it, and for SubMenu1 to update it directly. I have shown example code for this below. Of course, this is tight coupling between the two screens, but then it seems to be a tightly coupled requirement.

The Provider package is useful to separate the state from the UI. You can use this approach. You say "But suddenly, my form is no longer initialized each time Menu1 is called, but only once when the application starts". Surely Menu1 can make a call to a method to initialise the Provider when it is invoked? I have added example code for the Provider approach. Some brief notes:

  • I use MultiProvider, even though I only have one ChangeNotifierProvider, this would make your code easier to read
  • The ChangeNotifier FormData holds the form state
  • The parent form initialises the form state
  • Both forms call FormData methods to set and get the state

Long example code direct state follows:

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: FormTest(),
        ),
      ),
    );
  }
}

class FormTest extends StatefulWidget {

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

class _FormTestState extends State<FormTest> {
  int _selectedIndex = 0;
  final List<Widget> _options = [
    FormWidget(),
    PlaceholderWidget(Colors.deepOrange),
    PlaceholderWidget(Colors.green)
  ];
  void _onItemTap(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text('BottomNavigationBar Example'),
          backgroundColor: Colors.teal),
      body: Center(
        child: _options.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.home),
                label: 'Home',
                backgroundColor: Colors.teal),
            BottomNavigationBarItem(
                icon: Icon(Icons.person),
                label: 'Profile',
                backgroundColor: Colors.cyan),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Settings',
              backgroundColor: Colors.lightBlue,
            ),
          ],
          type: BottomNavigationBarType.shifting,
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.white,
          unselectedItemColor: Colors.grey,
          iconSize: 40,
          onTap: _onItemTap,
          elevation: 5),
    );
  }
}

class PlaceholderWidget extends StatelessWidget {
  final Color color;

  PlaceholderWidget(this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
    );
  }
}

class FormWidget extends StatefulWidget {
  @override
  FormWidgetState createState() {
    return FormWidgetState();
  }
}

class FormWidgetState extends State<FormWidget> {
  final _formKey = GlobalKey<FormState>();
  final _formData = {};

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter some text';
              }
              return null;
            },
            onSaved: (value) {
              _formData['parent'] = value;
            },
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 16.0),
            child: ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  _formKey.currentState!.save();
                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                      content: Text('Parent text: ' + _formData['parent'])));
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => SubFormWidget(_formData)),
                  );
                }
              },
              child: Text('SubForm'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 16.0),
            child: ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  _formKey.currentState!.save();
                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                      content: Text('Subform text: ' + _formData['child'])));
                }
              },
              child: Text('Submit'),
            ),
          ),
        ],
      ),
    );
  }
}

class SubFormWidget extends StatefulWidget {
  final Map _formData;
  SubFormWidget(this._formData);

  @override
  SubFormWidgetState createState() {
    return SubFormWidgetState();
  }
}

class SubFormWidgetState extends State<SubFormWidget> {
  final _subFormKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _subFormKey,
      child: Scaffold(
        appBar: AppBar(
            title: const Text('BottomNavigationBar Example'),
            backgroundColor: Colors.teal),
        body: Column(
          children: <Widget>[
            Text(widget._formData['parent']),
            TextFormField(
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter some text';
                }
                return null;
              },
              onSaved: (value) {
                widget._formData['child'] = value;
              },
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  if (_subFormKey.currentState!.validate()) {
                    _subFormKey.currentState!.save();
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(
                            'Subform text: ' + widget._formData['child'])));
                    Navigator.pop(context);
                  }
                },
                child: Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Long example code for Provider approach:

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 MultiProvider(
      providers: [
        ChangeNotifierProvider<FormData>(
          create: (ctx) => FormData(),
        ),
      ],
      child: MaterialApp(
        title: 'Test App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: FormTest2(),
      ),
    );
  }
}

class FormTest2 extends StatefulWidget {
  FormTest2({Key key}) : super(key: key);

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

class _FormTest2State extends State<FormTest2> {
  int _selectedIndex = 0;
  final List<Widget> _options = [
    FormWidget2(),
    PlaceholderWidget(Colors.deepOrange),
    PlaceholderWidget(Colors.green)
  ];
  void _onItemTap(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text('BottomNavigationBar Example 2'),
          backgroundColor: Colors.teal),
      body: Center(
        child: _options.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.home),
                label: 'Home',
                backgroundColor: Colors.teal),
            BottomNavigationBarItem(
                icon: Icon(Icons.person),
                label: 'Profile',
                backgroundColor: Colors.cyan),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Settings',
              backgroundColor: Colors.lightBlue,
            ),
          ],
          type: BottomNavigationBarType.shifting,
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.white,
          unselectedItemColor: Colors.grey,
          iconSize: 40,
          onTap: _onItemTap,
          elevation: 5),
    );
  }
}

class PlaceholderWidget extends StatelessWidget {
  final Color color;

  PlaceholderWidget(this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
    );
  }
}

class FormWidget2 extends StatefulWidget {
  @override
  FormWidget2State createState() {
    return FormWidget2State();
  }
}

class FormData extends ChangeNotifier {
  var _formData = <String, String>{};

  void init() {
    _formData = <String, String>{};
    // No notifyListeners() needed in this use case
  }

  void setEntry(String key, String value) {
    _formData[key] = value;
    // notifyListeners() may or may not be needed in this use case
    notifyListeners();
  }

  String getEntry(String key) {
    var value = _formData[key];
    return value;
  }
}

class FormWidget2State extends State<FormWidget2> {
  final _formKey = GlobalKey<FormState>();

  @override
  void initState() {
    Provider.of<FormData>(context, listen: false).init();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<FormData>(
      builder: (context, formData, _) => Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              initialValue: formData.getEntry('parent'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter some text';
                }
                return null;
              },
              onSaved: (value) {
                formData.setEntry('parent', value);
              },
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState.validate()) {
                    _formKey.currentState.save();
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(
                            'Parent text: ' + formData.getEntry('parent'))));
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => SubFormWidget2()),
                    );
                  }
                },
                child: Text('SubForm'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState.validate()) {
                    _formKey.currentState.save();
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(
                            'Subform text: ' + formData.getEntry('child'))));
                  }
                },
                child: Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SubFormWidget2 extends StatefulWidget {
  @override
  SubFormWidget2State createState() {
    return SubFormWidget2State();
  }
}

class SubFormWidget2State extends State<SubFormWidget2> {
  final _subFormKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _subFormKey,
      child: Scaffold(
        appBar: AppBar(
            title: const Text('BottomNavigationBar Example 2'),
            backgroundColor: Colors.teal),
        body: Consumer<FormData>(
          builder: (context, formData, _) => Column(
            children: <Widget>[
              Text(formData.getEntry('parent')),
              TextFormField(
                initialValue: formData.getEntry('child'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter some text';
                  }
                  return null;
                },
                onSaved: (value) {
                  formData.setEntry('child', value);
                },
              ),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 16.0),
                child: ElevatedButton(
                  onPressed: () {
                    if (_subFormKey.currentState.validate()) {
                      _subFormKey.currentState.save();
                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                          content: Text(
                              'Subform text: ' + formData.getEntry('child'))));
                      Navigator.pop(context);
                    }
                  },
                  child: Text('Submit'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Related