Reputation: 3759
My app has different parts and I want them to have different theme colors, including for all the subroutes in the navigation.
But if I use a Theme, it's not applied to the widgets in the subroutes. I also tried to use nested MaterialApps but this won't work because I can't pop back to the root menu. I'd prefer not to have to pass a Color parameter to all the screens. What should I do?
Here is a test code:
import 'package:flutter/material.dart';
main() {
runApp(MaterialApp(home: _Test()));
}
class _Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: Text('Red section'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Theme(
data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),
child: _TestSubRoute(),
);
},
));
},
),
const SizedBox(height: 16),
ElevatedButton(
child: Text('Green section'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Theme(
data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.green)),
child: _TestSubRoute(),
);
},
));
},
),
],
),
),
);
}
}
class _TestSubRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
appBar: AppBar(
title: Text('Should keep the same color through the navigation...'),
actions: [
IconButton(
icon: Icon(Icons.help),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Hello'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
},
),
],
),
body: Center(
child: ElevatedButton(
child: Text('Push...'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => _TestSubRoute()),
);
},
),
),
);
}
}
Upvotes: 5
Views: 7986
Reputation: 103551
I use another approach for this solution, InheritedWidget
.
Basically:
InheritedWidget
where you will call the updated theme.StatefulWidget
wrapper class that contains the InheritedWidget
and the MaterialApp
(where you going to update the Theme).data: ThemeData.light(),
.ThemeProvider.of(context).updateTheme(yournewtheme)
before pushing the new screen.Result:
This is the code I used, based on your example:
main() {
runApp(
ThemeWrapper(
child: _Test(),
),
);
}
class ThemeProvider extends InheritedWidget {
final ValueChanged<ThemeData> onThemeUpdated;
const ThemeProvider({
required this.onThemeUpdated,
required super.child,
super.key,
});
static ThemeProvider of(BuildContext context) =>
context.findAncestorWidgetOfExactType<ThemeProvider>()!;
void updateTheme(ThemeData data) {
onThemeUpdated(data);
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) => false;
}
class ThemeWrapper extends StatefulWidget {
const ThemeWrapper({required this.child, super.key});
final Widget child;
@override
State<ThemeWrapper> createState() => _ThemeWrapperState();
}
class _ThemeWrapperState extends State<ThemeWrapper> {
ThemeData? _theme;
@override
Widget build(BuildContext context) {
return ThemeProvider(
onThemeUpdated: (newTheme) {
setState(() {
_theme = newTheme;
});
},
child: MaterialApp(
theme: _theme,
home: _Test(),
),
);
}
}
class _Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData.light(),
child: Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: const Text('Red section'),
onPressed: () {
ThemeProvider.of(context).updateTheme(
ThemeData(
colorScheme: const ColorScheme.light(primary: Colors.red),
),
);
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return _TestSubRoute();
},
));
},
),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('Green section'),
onPressed: () {
ThemeProvider.of(context).updateTheme(
ThemeData(
colorScheme:
const ColorScheme.light(primary: Colors.green),
),
);
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return _TestSubRoute();
},
));
},
),
],
),
),
),
);
}
}
class _TestSubRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
appBar: AppBar(
title:
const Text('Should keep the same color through the navigation...'),
actions: [
IconButton(
icon: const Icon(Icons.help),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Hello'),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
},
),
],
),
body: Center(
child: ElevatedButton(
child: const Text('Push...'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => _TestSubRoute()),
);
},
),
),
);
}
}
Upvotes: 3
Reputation: 4341
Instead of creating multiple MaterialApp, you can wrap your scaffold in Navigator and Theme widget. This way, same theme will be applied to all the sub-routes.
Now, if you want to pop to root navigator, you may choose to do
Navigator.of(context, rootNavigator: true).pop();
This will pop the entire nested Navigators.
Upvotes: 0
Reputation: 3759
Here is the solution that I finally found. Each section is in a MaterialApp with its own ThemeData. To get back to the root screen, I use a BackButton with
Navigator.of(context, rootNavigator: true).pop()
Contrary to other solutions, this does not force to change all sub-screens, but only the first screen of each section.
Upvotes: 1
Reputation: 305
Please find a nice workaround below. The idea is - you get the theme of the route and then apply it to the subroutes.
main() {
runApp(MaterialApp(home: _Test()));
}
class _Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: Text('Red section'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Theme(
data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),
child: _TestSubRoute(),
);
},
));
},
),
const SizedBox(height: 16),
ElevatedButton(
child: Text('Green section'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Theme(
data: ThemeData(colorScheme: ColorScheme.light(primary: Colors.green)),
child: _TestSubRoute(),
);
},
));
},
),
],
),
),
);
}
}
class _TestSubRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
appBar: AppBar(
title: Text('Should keep the same color through the navigation...'),
actions: [
IconButton(
icon: Icon(Icons.help),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Hello'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
},
),
],
),
body: Center(
child: ElevatedButton(
child: Text('Push...'),
onPressed: () {
final theme = Theme.of(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Theme(
data: theme,
child: _TestSubRoute()
),
),
);
},
),
),
);
}
}```
Upvotes: 0
Reputation: 450
I ran your code - You are calling Navigator.push() on _TestSubRoute() from _Test and supplying ThemeData.
You are then calling Navigator.push()on _TestSubRoute() from within _TestSubRoute() itself but not supplying ThemeData.
If you really just want to add a global theme you can do it in your main like this:
main() {
runApp(MaterialApp(home: _Test(), theme: ThemeData(colorScheme: ColorScheme.light(primary: Colors.red)),));
}
Otherwise, I've noticed that both your widgets are Stateless which means that they will not change or hold state based on Theme.data which is only scoped within Navigator.push()in _Test()
If you want to dynamically change themes for _TestSubRoute(), you will need to use stateful widgets and store ThemeData in the parent widget and change that based on your requirement e.g., onPressed(), onTap() etc.
If you want to still want use just stateless widgets for trivial apps, you will need to specify the ThemeData each time you use Navigator.push() as flutter is not going to set any state for those widgets.
If you want to add a global theme as mentioned by other answers, in your code you can do it like this:
Here is some info from the flutter website:
A widget is either stateful or stateless. If a widget can change—when a user interacts with it, for example—it’s stateful.
A stateless widget never changes. Icon, IconButton, and Text are examples of stateless widgets. Stateless widgets subclass StatelessWidget.
A stateful widget is dynamic: for example, it can change its appearance in response to events triggered by user interactions or when it receives data. Checkbox, Radio, Slider, InkWell, Form, and TextField are examples of stateful widgets. Stateful widgets subclass StatefulWidget.
Hope this helps!
Upvotes: 0
Reputation: 9206
you can set a custom different ThemeData
to a part of the widget tree using the Theme
widget like this:
Theme(
data: ThemeData.light().copyWith(
primaryColor: Colors.purple,
),
child: YourWidget(),
);
in Flutter the Theme
is an InheritedWidget
so that the ThemeData
you set to the data
property will be applied to the whole subtree of the child property, even if you separate your widget into multiples widgets..., they will refer to the closet Theme
widget in the widget tree using the context
.
the MaterialApp
has the default one nested inside of it, so when we do the:
Theme.of(context).primaryColor,
it gets us the primaryColor
from the MaterialApp
's nested Theme
since it's the closet.
but when you specify a Theme widget to a part of your widget tree like the example above, anything inside the YourWidget()
will refer to that ThemeData
see https://api.flutter.dev/flutter/material/Theme-class.html
Upvotes: 8