Search code examples
androidflutterflutter-provider

Use Provider to update Scaffold from a second screen


I want to have a Settings screen where I can choose a color to be returned to the first screen.

I can't get the first screen to update when the Setting screen is closed.

I'm using the Provider as a change notifier. But I can't see how to trigger the update of the first screen. The third button creates an event which updates the screen, but can this be done automatically?

What am I missing...?

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

void main() => runApp(MyApp());
Color bgColor = Colors.yellow[100];

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: MyHomeScreen());
  }
}

class MyHomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => ColorModel()),
      ],
      child: Consumer<ColorModel>(builder: (context, colorModel, child) {
        return Scaffold(
          appBar: AppBar(title: Text('Thanks for your help :)')),
          body: Container(
            constraints: BoxConstraints.expand(),
            color: bgColor,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('Change background color on this screen'),

                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.green[600],
                  ),
                  child:
                  Text('Button1', style: TextStyle(color: Colors.white)),
                  onPressed: () {
                    var result = Navigator.push(
                        context, MaterialPageRoute(builder: (context) => Screen2()));
                    print('>>> Button1-onPressed completed, result=$result');
                  },
                ),


                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.green[600],
                  ),
                  child:
                  Text('Choose a colour', style: TextStyle(color: Colors.white)),
                  onPressed: () {
                    asyncButton(context);
                    print('>>> Screen1 Button-onPressed completed');
                  },
                ),

                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.green[600],
                  ),
                  child:
                  Text('Now try me', style: TextStyle(color: Colors.white)),
                  onPressed: () {
                    colorModel.notifyListeners();
                  },
                ),
              ],
            ),
          ),
        );
      }),
    );
  }

  void asyncButton(BuildContext context) async {
    var result = await Navigator.push(
        context, MaterialPageRoute(builder: (context) => Screen2()));
    print('>>> asyncButton completed: result = $result');
    bgColor = result;
  }
}

class ColorModel with ChangeNotifier {
  void updateDisplay() {
    notifyListeners();
  }
}

class Screen2 extends StatelessWidget {
  int _value;
  List<String> names = ['Red', 'Green', 'Blue'];
  List<Color> colors = [Colors.red[100], Colors.green[100], Colors.blue[100]];

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => ColorModel()),
      ],
      child: Scaffold(
        appBar: AppBar(
          toolbarHeight: 80,
          backgroundColor: Colors.blue,
          title: Center(child: Text('Screen2')),
        ),
        body: Container(
          constraints: BoxConstraints.expand(),
          color: Colors.white,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Consumer<ColorModel>(builder: (context, colorModel, child) {
                return DropdownButton(
                  value: _value,
                  hint: Text("Select a color"),
                  focusColor: Colors.lightBlue,
                  onChanged: (int value) {
                    Navigator.pop(context, colors[value]);
                  },
                  items: [
                    DropdownMenuItem(value: 0, child: Text(names[0])),
                    DropdownMenuItem(value: 1, child: Text(names[1])),
                    DropdownMenuItem(value: 2, child: Text(names[2])),
                  ],
                );
              }),
            ],
          ),
        ),
      ),
    );
  }
}


Solution

  • Navigator.push is tricky to use with Provider. It causes a lot of "Could not find the correct Provider above this Navigator Widget" errors. I've explained why in this answer to a related question.

    Here's a quick overview of your situation:

    Provider Scope

    Architecture in question code:

    MaterialApp
     > provider(Screen A)
     > provider(Screen B)
    

    Architecture in solution below:

    provider(MaterialApp)
     > Screen A
     > Screen B
    

    Here's your code sample, shortened up, working with Provider, updating the background color on Page 1 from the Page 2.

    I've put comments throughout the code to explain changes.

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    // - global var removed -
    // Color bgColor = Colors.yellow[100];
    
    void main() {
      runApp(ProviderApp());
    }
    
    class ProviderApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        /// Define your Provider here, above MaterialApp
        return ChangeNotifierProvider(
          create: (context) => ColorModel(),
          child: MaterialApp(
              title: 'Flutter Demo',
              debugShowCheckedModeBanner: false,
            home: ScreenA()
          ),
        );
      }
    }
    
    class ScreenA extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Thanks for your help :)')),
          body: Container(
            constraints: BoxConstraints.expand(),
            //
            // color: bgColor // - global var removed -
            color: Provider.of<ColorModel>(context).bgColor,
            // ↑ use your Provider state-stored value here ↑
            //
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('Change background color on this screen'),
                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.green[600],
                  ),
                  child: Text('Go Screen B', style: TextStyle(color: Colors.white)),
                  // Navigator.push returns a Future, must async/await to use return value
                  onPressed: () async {
                    var result = await Navigator.of(context).push(
                        MaterialPageRoute(builder: (context) => ScreenB()));
                    // note that this context is not Screen A context, but MaterialApp context
                    // see https://stackoverflow.com/a/66485893/2301224
                    print('>>> Button1-onPressed completed, result=$result');
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    
    /// This is your state object. Store your state here.
    /// Create this once and use anywhere you need.  Don't re-create this unless
    /// you want to wipe out all state data you were holding/sharing.
    class ColorModel with ChangeNotifier {
      // color is the state info you want to store & share
      Color bgColor = Colors.yellow[100]; // initialized to yellow
    
      /// Update your state value and notify any interested listeners
      void updateBgColor(Color newColor) {
        bgColor = newColor;
        notifyListeners();
      }
    
      /// - removed - replaced with updateBgColor ↑
      /*void updateDisplay() {
        notifyListeners();
      }*/
    }
    
    class ScreenB extends StatelessWidget {
      // all fields in StatelessWidgets should be final
      //final int value;  // this value isn't needed
      final List<String> names = ['Red', 'Green', 'Blue'];
      final List<Color> colors = [Colors.red[100], Colors.green[100], Colors.blue[100]];
    
      @override
      Widget build(BuildContext context) {
        /// Instantiating your model & giving it to Provider to should only happen once per
        /// Widget Tree that needs access to that state. e.g. MaterialApp for this solution
        /// The state object & Provider below was repeated & has been commented out / removed.
        /// This was wiping out any previously stored state and creating a new Provider / Inherited scope
        /// to all children.
        /*return MultiProvider(
          providers: [
            ChangeNotifierProvider(create: (context) => ColorModel()),
          ],
          child: ,
        );*/
        // - end of duplicate Provider removal -
        return Scaffold(
          appBar: AppBar(
            title: Text('Screen2'),
          ),
          body: Container(
            alignment: Alignment.center,
            child: Consumer<ColorModel>(builder: (context, colorModel, child) {
              return DropdownButton(
                //value: value, // this value isn't needed
                hint: Text("Select a color"),
                onChanged: (int value) {
                  colorModel.updateBgColor(colors[value]);
                  Navigator.pop(context, colors[value]);
                },
                items: [
                  DropdownMenuItem(value: 0, child: Text(names[0])),
                  DropdownMenuItem(value: 1, child: Text(names[1])),
                  DropdownMenuItem(value: 2, child: Text(names[2])),
                ],
              );
            }),
          ),
        );
      }
    }