Navigation problem with FutureBuilder and MaterialApp

3,193

Solution 1

Now try the following. Try to make a root widget separately, because root widget is always there. you don't want a complete UI route to persist in the memory. Also make next route as a separate widget.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Test(),
    );
  }
}

class Test extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TestState();
}

class _TestState extends State<Test> {
  Future<Color> color = Model.getColor();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Color>(
      future: color,
      builder: (context, snapshot) {
        if (snapshot.hasError) return Center(child: Text("An Error Occurred"));
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildWait();
        }
        var app = Theme(
          data: ThemeData(primaryColor: snapshot.data),
          child: _buildPage(),
        );
        return app;
      },
    );
  }

  Widget _buildPage() {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text('Push'),
          onPressed: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return NextRoute();
            }));
          },
        ),
      ),
    );
  }
}

Widget _buildWait() {
  return Scaffold(
    appBar: AppBar(title: Text('Wait...')),
    body: Center(child: CircularProgressIndicator()),
  );
}

class Model {
  static final _colors = [Colors.red, Colors.green, Colors.amber];
  static int _index = 0;
  static Future<Color> getColor() {
    return Future.delayed(
        Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
  }
}

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

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

class _NextRouteState extends State<NextRoute> {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Color>(
        future: Model.getColor(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Text("An Error Occurred"),
            );
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return _buildWait();
          }

          return Theme(
            data: ThemeData(primaryColor: snapshot.data),
            child: Scaffold(
              appBar: AppBar(),
            ),
          );
        });
  }
}

Solution 2

so I think I have found the error, actually you must have an MaterialApp inside runApp() as root.

so you can't have MaterialApp inside FutureBuilder what you can do is make MaterialApp the root widget and have a default Home Screen and inside its build method you can have your FutureBuilder but again don't include materialApp inside it just use Scaffold directly.

EDIT : To answer the question regarding app theme

You can have switching themes by using theme and darkTheme in materialApp And control themeMode from Provider or any other state management approach.

MaterialApp(
          title: 'Flutter Tutorials',
          debugShowCheckedModeBanner: false,
          theme: AppTheme.lightTheme,
          darkTheme: AppTheme.darkTheme,
          themeMode: appState.isDarkModeOn ? ThemeMode.dark : ThemeMode.light,
          home: ThemeDemo(),
        );

There are several ways to do it here is one more that I found custom theme app

Try this out it will work, if doesn't let me know

Solution 3

I think you can do as follow:

  1. Move all Future data/FutureBuilder into different Stateless/Stateful Widget and override the theme color with Theme class

    class SecondPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        var color = Theme.of(context).primaryColor;
    
        return FutureBuilder(
          future: Model.getColor(),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return Theme(
                data: ThemeData(primaryColor: color), 
                child: _buildWait(),
              );
            } else {
              color = snapshot.data;
              return Theme(
                data: ThemeData(primaryColor: color),
                child: Scaffold(
                  appBar: AppBar(),
                ),
              );
            }
          },
        );
      }
    }
    
  2. The first page use the local variable to store color

    ...
    Color _primaryColor;
    
    @override
    void initState() {
      _primaryColor = Theme.of(context).primaryColor;
      super.initState();
    }
    
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        theme: ThemeData(primaryColor: _primaryColor),
        home: _buildPage(),
      );
    }
    ...
    
  3. If you want the first page update the theme on the same time, you should use some method to share data between widget (e.g. Provider). I use the simple method to catch the custom return value

    // First Page
    // use "then" can get the return value from the other route
    ...
    onPressed: () {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return SecondPage();
      })).then((color) {
        setState(() {
          _primaryColor = color;
        });
      });
    },
    ...
    
    // Second Page
    // WillPopScope can catch navigator pop event
    ...
    return WillPopScope(
      onWillPop: () async {
        Navigator.of(context).pop(color);
        return Future.value(false);
      },
      child: FutureBuilder(
       ...
    

If it is not necessary for you to use Routing when you try to change Theme, I can provide a simple solution that change the theme data by Theme class

ThemeData currentTheme;

@override
void initState() {
  super.initState();
  currentTheme = Theme.of(context);
}


@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: FutureBuilder<Color>(
      future: color,
      builder: (context, snapshot) {
        Widget child;
        if (snapshot.hasError) throw snapshot.error;
        if (snapshot.connectionState != ConnectionState.done) {
          child =  Scaffold(
            appBar: AppBar(),
            body: const Center(child: CircularProgressIndicator()),
          );
        }else{
          currentTheme = currentTheme.copyWith(primaryColor: snapshot.data);
          child = _buildPage();
        }
        return Theme(
          data: currentTheme,
          child: child,
        );
      },
    ),
  );
}

Here is the document of Themes for part of an application

Share:
3,193
Patrick
Author by

Patrick

Updated on November 27, 2022

Comments

  • Patrick
    Patrick over 1 year

    My app has a state which is computed as a Future. For example it includes a theme color, because I want to change the color when I navigate. I try to display a progress indicator while waiting for the data.

    But I can't make it work. Either Navigator.push is not working and the app bar is missing, or I have no progress indicator and a route error...

    Here is a code snippet.

    import 'package:flutter/material.dart';
    
    void main() => runApp(Test());
    
    class Test extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _TestState();
    }
    
    class _TestState extends State<Test> {
      Future<Color> color = Model.getColor();
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<Color>(
          future: color,
          builder: (context, snapshot) {
            if (snapshot.hasError) throw snapshot.error;
            if (snapshot.connectionState != ConnectionState.done) {
              if (false) {
                // Navigation not working. App bar missing.
                return Material(child: Center(child: CircularProgressIndicator()));
              } else {
                // Progress not working. Screen flickering.
                return MaterialApp(home: _buildWait());
              }
            }
            var app = MaterialApp(
              theme: ThemeData(primaryColor: snapshot.data),
              home: _buildPage(),
              // ERROR: The builder for route "/" returned null.
              // routes: {'/': (_) => _buildPage()},
            );
            return app;
          },
        );
      }
    
      Widget _buildPage() {
        return Builder(
          builder: (context) {
            return Scaffold(
              appBar: AppBar(),
              body: Center(
                child: RaisedButton(
                  child: Text('Push'),
                  onPressed: () {
                    setState(() {
                      color = Model.getColor();
                    });
                    Navigator.push(context, MaterialPageRoute(builder: (context) {
                      return Scaffold(appBar: AppBar());
                    }));
                  },
                ),
              ),
            );
          },
        );
      }
    }
    
    Widget _buildWait() {
      return Scaffold(
        appBar: AppBar(title: Text('Wait...')),
        body: Center(child: CircularProgressIndicator()),
      );
    }
    
    class Model {
      static final _colors = [Colors.red, Colors.green, Colors.amber];
      static int _index = 0;
      static Future<Color> getColor() {
        return Future.delayed(Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
      }
    }
    

    Expected result: when I push the button to navigate to the new route, it should display a progress indicator, and then the new screen with a different theme color.