Navigation problem with FutureBuilder and MaterialApp
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:
-
Move all Future data/FutureBuilder into different Stateless/Stateful Widget and override the theme color with
Theme
classclass 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(), ), ); } }, ); } }
-
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(), ); } ...
-
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
Patrick
Updated on November 27, 2022Comments
-
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.