logo
icon_menu
  1. Home
  2. Categories
  3. Flutter

Tutorial: Simple Themes in Flutter using Riverpod

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.

Have a look at the steps I make for every Flutter app: 5 Things I do when starting a new Flutter project.

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:

name: theming description: bettercoding.dev - Theming publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_riverpod: ^0.14.0+3 dev_dependencies: flutter_test: sdk: flutter flutter: assets: - assets/
Code language: YAML (yaml)

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 ThemeDatas and create some utility classes and functions.

Let’s start with defining our themes.

import 'package:flutter/material.dart'; // THEME PROVIDERS // THEMES // light final _theme = ThemeData(); //dark final _darkTheme = ThemeData(); // EXTENSIONS AND CLASSES
Code language: Dart (dart)

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.

import 'package:flutter/material.dart'; // THEME PROVIDERS // THEMES // light final _theme = ThemeData( primaryColor: Colors.deepPurple, elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( primary: Colors.deepPurple, shape: StadiumBorder(), ), ), textTheme: TextTheme( headline1: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, color: Colors.black87, ), headline2: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black54, )), ); //dark final _darkTheme = ThemeData( brightness: Brightness.dark, primaryColor: Colors.deepPurple, elevatedButtonTheme: _theme.elevatedButtonTheme, textTheme: _theme.textTheme.apply( displayColor: Colors.white, bodyColor: Colors.white, ), ); // EXTENSIONS AND CLASSES
Code language: Dart (dart)

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:

class CustomThemeData { final double imageSize; CustomThemeData({ this.imageSize = 100, }); }
Code language: Dart (dart)

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.

import 'package:flutter/material.dart'; // THEME PROVIDERS // THEMES // light final _theme = ThemeData(...); final _customTheme = CustomThemeData( imageSize: 150, ); //dark final _darkTheme = ThemeData(...); final _customDarkTheme = CustomThemeData(); // EXTENSIONS AND CLASSES ...
Code language: Dart (dart)

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.

extension CustomTheme on ThemeData { CustomThemeData get custom => brightness == Brightness.dark ? _customDarkTheme : _customTheme; }
Code language: Dart (dart)

Now, our custom theme attributes can be accessed using:

Theme.of(context).custom.imageSize
Code language: Dart (dart)

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:

// THEME PROVIDERS final theme = Provider((ref) => _theme); final darkTheme = Provider((ref) => _darkTheme);
Code language: Dart (dart)

Next, we continue in the main.dart file. A very basic implementation could look like this:

void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Container(); } }
Code language: Dart (dart)

First, we need to define a ProviderScope. This is required by Riverpod to work. Afterwards, our main function is defined this way:

void main() { runApp(ProviderScope(child: MyApp())); }
Code language: Dart (dart)

So far, the app won’t do much. Let’s replace the Container with something more useful.

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( builder: (context, watch, _) => MaterialApp( theme: watch(theme), darkTheme: watch(darkTheme), home: Container(), ), ); } }
Code language: Dart (dart)

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 Providers.

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:

import 'package:flutter/material.dart'; class MainPage extends StatelessWidget { const MainPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('bettercoding.dev – theming'), ), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), Text( 'Welcome!', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline1, ), const SizedBox(height: 16), Text( 'This is a tutorial about theming.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline2, ), Spacer(), ElevatedButton( child: Text('Click me!'), onPressed: () {}, ) ], ), ), ); } }
Code language: Dart (dart)

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:

assets folder structure

In theme.dart, we add a function imageForName to the CustomTheme extension.

// EXTENSIONS AND CLASSES extension CustomTheme on ThemeData { AssetImage imageForName(String name) { final path = brightness == Brightness.dark ? 'assets/dark' : 'assets/'; return AssetImage('$path/$name'); } ... }
Code language: Dart (dart)

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:

Image( image: Theme.of(context).imageForName('logo.png'), height: Theme.of(context).custom.imageSize, ), Spacer(),
Code language: JavaScript (javascript)

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);
Code language: JavaScript (javascript)

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:

MaterialApp( theme: watch(theme), darkTheme: watch(darkTheme), themeMode: watch(themeMode).state, home: MainPage(), )
Code language: Dart (dart)

And finally, let’s implement the onPressed callback of our ElevatedButton in main_page.dart.

ElevatedButton( child: Text('Click me!'), onPressed: () { final state = context.read(themeMode).state; context.read(themeMode).state = state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; }, )
Code language: Dart (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.

Further readings

Should I use themes or not? This is one of the questions you should ask yourself when starting a new Flutter project.

Comments

Valentin says:

Great article, thank you very much.
I have one question though: What if i would like to extend the textThemes as well, let’s say with a new one named ‘heading47’, which should have a different color value depending on if the light or dark theme is chosen?
How would that look like with your CustomThemeData solution?
Thanks a lot for your answer.
Best regards
Valentin

Stefan says:

You could just add your heading47 to the CustomThemeData and access it like
Text('hello world', style: Theme.of(context).custom.heading47).

Leave a Reply

Your email address will not be published. Required fields are marked *