Use Provider to update Scaffold from a second screen

Issue

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

Answered By – Baker

Answer Checked By – Marilyn (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.