Adding a dark and a light mode is becoming increasingly popular in app development. In this tutorial, we will
write a Flutter app with a light and a dark theme and extend Flutter’s ThemeData
with custom attributes.
Goal of this Tutorial
In this tutorial, we will have a look at how to add a dark and a light theme to your Flutter project. Further, we will implement the functionality to switch between dark and light mode directly within your app.
Since often it is necessary to have custom, theme-specific attributes, we will have a look on how to solve this problem. This tutorial also shows how to load theme-specific assets.
You can check out the code for this tutorial on GitHub. Use
the template
branch as a starting point, or create your own project. The finished app is on branch solution
.
Prerequisites
Before we start, we need to create a new Flutter project. We will be using a Flutter version supporting null-safety for this tutorial.
Furthermore, we need to add flutter_riverpod
as dependency.
Lastly, we need to create an asset
folder right in your root directory and reference it in our pubspec.yaml
file.
This allows us to load images from this directory later.
The pubspec.yaml
should look like this:
|
|
Dark and Light Theme: Specifying the look
Next, we create a theme.dart
file in our lib
folder. This file will contain the heart of our app. Here we will
specify our ThemeData
s and create some utility classes and functions.
Let’s start with defining our themes.
|
|
Here you can be creative how you want to style the app. This is how I defined my _theme
and _darkTheme
. Just make
sure you set the brightness
of your ThemeData
correctly, so that the correct fallback values can be used.
|
|
Custom Attributes: Extending ThemeData
Often, you want to have additional theme-specific attributes, but extending ThemeData
does not really work well (
if at all). For this tutorial, we are going to assume that we want to size our images differently on light and on dark
mode.
To solve this problem, we create a new class CustomThemeData
right under // EXTENSIONS AND CLASSES
and add an
imageSize
attribute.
The class should look like this:
|
|
A size of 100 is used as the default value. Add any attributes to this class that you need in your app.
Now we create instances of CustomThemeData
for both, light and dark theme and put them right below their corresponding
ThemeData
instances.
|
|
In this case, images will have a size of 150 in the light theme, whereas the dark theme will fall back on the default value of 100.
To glue it all together, we create an extension that makes our CustomThemeData
available to use.
|
|
Now, our custom theme attributes can be accessed using:
|
|
Amazing, right?
Providing our themes using Riverpod
After creating the style of our themes, we need to provide them to our MaterialApp
widget. We are going to use *
Riverpod* for this purpose. If you are not familiar with Riverpod checkout riverpod.dev.
We create following two providers below // THEME PROVIDERS
:
|
|
Next, we continue in the main.dart
file. A very basic implementation could look like this:
|
|
First, we need to define a ProviderScope
. This is required by Riverpod to work. Afterwards, our main
function is
defined this way:
|
|
So far, the app won’t do much. Let’s replace the Container
with something more useful.
|
|
We need to wrap our MaterialApp
with a Consumer
widget from the Riverpod package. Consumer
offers us the watch
function that we will use to read (‘watch’) the values offered by our Provider
s.
By default, Flutter uses the brightness (light or dark mode) from the underlying system. For now, we leave it like this, but we will return later to implement switching themes within the app ( see: Theme Switching: changing the Theme within the App).
Finally, it is time to create a page that we want to show when the app starts. We create a file main_page.dart
and add
a StatelessWidget
MainPage
.
Feel free to be creative here. I’ll go with the following code:
|
|
Don’t forget to replace the Container
with MainPage
at the home attribute of your MaterialApp
.
Now, let’s build our app.
So far, so good. A basic implementation of the app is ready. But there is more.
Theme-specific Assets: Different images for light and dark mode
Very often, it is necessary to use different versions of an image when working with themes. As with the text color, the color of an image in dark theme needs to change.
In the assets
folder, upload an image that you want to use for light mode and call it logo.png
. Create a subfolder
called dark
and upload the dark mode version of the image with the same name.
The folder structure should look like this:
In theme.dart
, we add a function imageForName
to the CustomTheme
extension.
|
|
The function checks the current brightness and builds the path to the asset accordingly. Let’s use this function to add
an image to the MainPage
.
Add the following lines between Spacer
and ElevatedButton
:
|
|
Don’t forget to import your theme.dart
file, so your extension functions can be used.
Here we make use of two extension functions we built earlier. imageForName
loads a theme-specific image from the
assets. Just pass the correct name of the file and the function resolves the correct asset for you.
custom.imageSize
lets us access a theme-specific attribute that we defined earlier.
Let’s build the app again and see what we have got.
We see that our app shows different images and adjusts the size according to what we have specified in our
CustomThemeData
.
Theme Switching: changing the Theme within the App
Finally, we are going to implement the functionality to switch between light and dark mode by clicking a button. For
this, we create a new StateProvider
in theme.dart
, just below the other providers.
final themeMode = StateProvider((ref) => ThemeMode.light);
A StateProvider
is a provider that holds a changeable state. We initialize this state with ThemeMode.light
. This
means, by default, our app will always open in light mode. This might not be ideal, but we’ll have a look at
alternatives in “Persisting the current Theme”.
Next, we go to main.dart
and set the themeMode
of our MaterialApp
widget. The widget should now look like this:
|
|
And finally, let’s implement the onPressed
callback of our ElevatedButton
in main_page.dart
.
|
|
The context.read
function – don’t forget to import the Riverpod library! – allows us to access the StateProvider
.
First, we read the state to see which mode is currently active. Then we assign a new value to the state – dark mode if
we are currently in light mode and vice versa.
Setting the state causes all widgets that are watching the themeMode
StateProvider
to rebuild, using the new value.
In our case, this is the MaterialApp
widget.
Clicking the button, switches between light and dark mode.
Voilà! We now can conveniently switch between the different themes from within our app. In a larger app, you would
probably add the logic setting the state of our themeMode
StateProvider
to your settings page.
Persisting the current Theme
One drawback this solution has is that the selected theme is reset once you restart the app. In order to keep the state over time, it is necessary to persist it at some point. Using shared preferences would be a possible solution.
I have implemented a solution in the persist_theme
branch of
the project repository. If you would like me to
add a tutorial on how I did this, let me know by leaving a comment.
Finally
In this tutorial, we saw how light and dark mode can be implemented in Flutter using Riverpod. Additionally, we
created a way to extend ThemeData
with custom attributes and how to load theme-specific assets.
Like always, there are certainly other possible solutions as well. If you want me to implement this example using different packages than Riverpod, or if you have any suggestions to my solution, write me a comment below.