4

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.

Patrick
  • 3,578
  • 5
  • 31
  • 53
  • You use a unnamed route. Actually every `named route` and `unnamed route` should be the descendant of `MaterialApp`, which means you can not change MaterialApp as you did because all routes are split from it. See the example of `named route` here: https://flutter.dev/docs/cookbook/navigation/named-routes – yellowgray Nov 22 '20 at 06:54
  • @yellowgray With named routes I have the error `The builder for route "/" returned null.` – Patrick Nov 23 '20 at 15:34
  • hi @Patrick, what I have understand from your question is that, lets say you have 3 screens A,B, and C. All have AppBar, and you want different app bar color in every screen. right? lets say user navigate form A to B, B will have different NavBar color, loading widget and back button on AppBar. right ? – Faiizii Awan Nov 25 '20 at 14:09

4 Answers4

1

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

Akshit Ostwal
  • 451
  • 3
  • 14
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(),
            ),
          );
        });
  }
}
Taha Malik
  • 2,188
  • 1
  • 17
  • 28
1

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

yellowgray
  • 4,006
  • 6
  • 28
  • Yes I need to have different themes for different routes of the app, sorry. – Patrick Nov 23 '20 at 18:20
  • Can you describe more detail `what happen/expected result` during route changing? Is future data ready before or after changing route? – yellowgray Nov 24 '20 at 05:40
0

I have removed the return Builder section in the _buildPage widget and it seems to work. It also shows the CircularProgressIndıcator and Wait... text in AppBar.

Here is the edited code:

Widget _buildPage() {
  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());
            }));
        },
      ),
    ),
  );
}
harundemir918
  • 432
  • 1
  • 4
  • 14