6

I have a app class that returns a MaterialApp() which has it's home set to TheSplashPage(). This app listens to the preferences notifier if any preferences are changed.

Then in TheSplashPage() I wait for some conditionals to be true and if they are I show them my nested material app.

Side Note: I use a material app here because it seems more logical since it has routes that the parent material app shouldn't have. And also once the user is unauthenticated or gets disconnected I want the entire nested app to shut down and show another page. This works great!

But my problem is the following. Both apps listen to ThePreferencesProvider() so when the theme changes they both get notified and rebuild. But this is a problem because whenever the parent material app rebuilds, it returns the splash page. So now I am back on TheSplashPage() whenever I change a setting on TheSettingsPage().

So my question is how can I stop my application from going back to the TheSplashPage() whenever I change a setting?

Main.dart

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

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setEnabledSystemUIOverlays([]);

    return MultiProvider(
      providers: [
        ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
        ChangeNotifierProvider<ConnectionProvider>(
          create: (_) => ConnectionProvider(),
        ),
        ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
      ],
      child: Consumer<PreferencesProvider>(builder: (context, preferences, _) {
        return MaterialApp(
          home: TheSplashPage(),
          theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
          debugShowCheckedModeBanner: false,
        );
      }),
    );
  }
}

TheSplashPage.dart

class TheSplashPage extends StatelessWidget {
  static const int fakeDelayInSeconds = 2;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
        builder: (context, delaySnapshot) {
          return Consumer<ConnectionProvider>(
              builder: (BuildContext context, ConnectionProvider connectionProvider, _) {

            if (delaySnapshot.connectionState != ConnectionState.done ||
                connectionProvider.state == ConnectionStatus.uninitialized) return _buildTheSplashPage(context);

            if (connectionProvider.state == ConnectionStatus.none) return TheDisconnectedPage();

            return Consumer<AuthenticationProvider>(
                builder: (BuildContext context, AuthenticationProvider authenticationProvider, _) {
              switch (authenticationProvider.status) {
                case AuthenticationStatus.unauthenticated:
                  return TheRegisterPage();
                case AuthenticationStatus.authenticating:
                  return TheLoadingPage();
                case AuthenticationStatus.authenticated:
                  return MultiProvider(
                    providers: [
                      Provider<DatabaseProvider>(create: (_) => DatabaseProvider()),
                    ],
                    child: Consumer<PreferencesProvider>(
                        builder: (context, preferences, _) => MaterialApp(
                              home: TheGroupManagementPage(),
                              routes: <String, WidgetBuilder>{
                                TheGroupManagementPage.routeName: (BuildContext context) => TheGroupManagementPage(),
                                TheGroupCreationPage.routeName: (BuildContext context) => TheGroupCreationPage(),
                                TheGroupPage.routeName: (BuildContext context) => TheGroupPage(),
                                TheSettingsPage.routeName: (BuildContext context) => TheSettingsPage(),
                                TheProfilePage.routeName: (BuildContext context) => TheProfilePage(),
                                TheContactsPage.routeName: (BuildContext context) => TheContactsPage(),
                              },
                              theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
                              debugShowCheckedModeBanner: false,
                            )),
                  );
              }
            });
          });
        });
  }

TheSettingsPage.dart

Switch(
  value: preferences.isDarkMode,
  onChanged: (isDarkmode) => preferences.isDarkMode = isDarkmode,
),
anonymous-dev
  • 2,897
  • 9
  • 48
  • 112

3 Answers3

9

You fell for the XY problem

The real problem here is not "my widget rebuilds too often", but "when my widget rebuild, my app returns to the splash page".

The solution is not to prevent rebuilds, but instead to change your build method such that it fixes the issue, which is something that I detailed previously here: How to deal with unwanted widget build?

You fell for the same issue as in the cross-linked question: You mis-used FutureBuilder.

DON'T:

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    // BAD: will recreate the future when the widget rebuild
    future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
    ...
  );
}

DO:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  // Cache the future in a StatefulWidget so that it is created only once
  final fakeDelayInSeconds = Future<void>.delayed(const Duration(seconds: 2));

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Rebuilding the widget no longer recreates the future
      future: fakeDelayInSeconds,
      ...
    );
  }
}
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
5

When using Consumer, you are forcing the widget to rebuild every time you notify listeners.

To avoid such behaviour, you can use Provider.of as stated in ian villamia's answer, as it can be used wherever you need it, and only where you need it.

The changes in your code to use Provider.of would be removing the consumer and adding Provider.of when resolving the theme as follows:

theme: Provider.of<PreferencesProvider>(context).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,        

HOWEVER if you want to keep using Consumer, you can do something else:

The child property on the Consumer widget is a child that is not rebuilt. You can use this to set the TheSpashScreen there, and pass it to the materialApp through the builder.

TL:DR

Use Provider.of if you need only to tap into one variable for simplicity.

Use Consumer with its child property as the child doesn't rebuild. <= Better performance

Using Provider.of

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);

return MultiProvider(
  providers: [
    ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
    ChangeNotifierProvider<ConnectionProvider>(
      create: (_) => ConnectionProvider(),
    ),
    ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
  ],
  child: Builder(
    builder: (ctx) {
        return MaterialApp(
          home: TheSpashPage(),
          theme: Provider.of<PreferencesProvider>(ctx).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
            );
          }),
        );
    }
}

Using Consumer

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);

return MultiProvider(
  providers: [
    ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
    ChangeNotifierProvider<ConnectionProvider>(
      create: (_) => ConnectionProvider(),
    ),
    ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
  ],
  child: Consumer<PreferencesProvider>(
    child: TheSpashPage(),
    builder: (context, preferences, child) {
        return MaterialApp(
          home: child,
          theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
          debugShowCheckedModeBanner: false,
            );
          }),
        );
    }
}

I hope this is helpful for you!

3

basically there's 2 ways in using a provider

  1. one it the current one you're using which is the consumer type,
  2. is using the instance of a provider
 final _preferencesProvider= Provider.of<PreferencesProvider>(context, listen: false);

you can toggle the "listen:true" if you want the widget to rebuild when notifyListeners() are called... false if otherwise also just use _preferencesProvider.someValue like any other instance

Dean Villamia
  • 576
  • 11
  • 24