Search code examples
flutterdartnavigationfutureflutter-navigation

Navigation problem with FutureBuilder and MaterialApp


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.


Solution

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