Following on from my last post about the potential pitfalls of Flutter themes, let’s look at some of the benefits. Looking at articles on Medium and answers on StackOverflow it seems that a lot of people fail to grasp the power of themes, and apply styles in an ad-hoc fashion at the widget or property level. This is fine for simple apps, but as app complexity grows it reduces code readability becomes increasingly difficult to maintain. What happens if you decide that all of your headings are exactly the wrong shade of yellow? You need to find and replace every single instance individually.
Used properly, however, the theme is your friend and allows you to create a consistent app which can turn on a dime. So lets see how we can declare a top level theme which can be switched out in an instant.
Creating a switchable theme using StatefulWidget
A lot of apps allow the user to choose between a light and a dark theme. In Flutter this can be implemented at the top level. The key to it is declaring your theme in a stateful widget. Almost every example I see creates the top level app object by extending statelessWidget. This is fine if you want a single theme – but to get the power of a switchable theme, we need instead to extend statefulWidget.
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
DartHaving done this, we also need to set up the App state
class MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Themes Tester',
home: MyHomePage(title: 'Theme Tester'),
theme: ThemeData(), // Theme will go here
);
}
}
DartUsing State to determine the theme
At this point we’ve not done anything that we couldn’t have done with statelessWidget – so let’s start to think about how we can leverage the state to adjust the theme. In order to this we will use a boolean variable, _isDark to choose between a light and a dark theme. We will also set up some basic items in the ThemeData which will allow us to see an immediate change as we switch from light to dark.
class MyAppState extends State<MyApp> {
bool _isDark = false;
final Color _appPrimary = const Color(0xff002244);
final Color _appSecondary = const Color(0xff0077ee);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Themes Tester',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: _appPrimary),
useMaterial3: true,
appBarTheme: AppBarTheme(
backgroundColor: (_isDark) ? _appPrimary: _appSecondary,
foregroundColor: (_isDark) ? Colors.white : Colors.black,
),
textTheme: TextTheme(
bodyMedium: TextStyle(color:(_isDark) ? Colors.white : Colors.black,),
headlineMedium: TextStyle(color: (_isDark) ? _appSecondary : _appPrimary,),
),
scaffoldBackgroundColor: (_isDark) ? Colors.black : Colors.white,
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: (_isDark) ? _appSecondary : _appPrimary,
foregroundColor: (_isDark) ? _appPrimary : _appSecondary,
),
),
home: MyHomePage(title: 'Theme Tester'),
);
}
}
DartSo far so good – but how do we change _isDark to switch the theme. To achieve this we need to do two things. Firstly we must declare methods which read and change the app State
class MyAppState extends State<MyApp> {
bool _isDark = false;
final Color _appPrimary = const Color(0xff002244);
final Color _appSecondary = const Color(0xff0077ee);
bool getTheme() {
return _isDark;
}
void setTheme() {
setState(() {
_isDark = !_isDark;
});
}
DartSecondly we must find a way of exposing this method to the rest of our app. We can do this by passing a reference to myAppState to myHomePage.
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: (_isDark) ? _appSecondary : _appPrimary,
foregroundColor: (_isDark) ? _appPrimary : _appSecondary,
),
),
home: MyHomePage(title: 'Theme Tester', myAppState: this),
);
}
}
DartChanging the theme with the click of a button
Now all we need to do is put it into practice,
When you create a brand new Flutter app in Android Studio, it gives you the following basic skeleton for your home page (with copious comments removed for readability!)
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
DartTo implement our flexible theme we need to modify myHomePage to receive the reference to myAppState which we declared earlier.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title, required this.myAppState});
final String title;
final MyAppState myAppState;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
DartNow all we need to do is add a single widget to switch between light and dark theme, and then watch the magic unfold.
The Switch widget defined below gets its on/off position from the getTheme() method we declared in myAppState above – and when its own state changes it calls the setTheme method to invert _isDark and switch the theme.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Switch(
value: widget.myAppState.getTheme(),
onChanged: (value) {setState(() {
widget.myAppState.setTheme();
});}),
const Text(
'You have pushed the button this many times:',
),
Dart
Leave a Reply