Flutter: Switch Themes with Provider (and persist). Step by step guide.

Flutter: Switch Themes with Provider (and persist). Step by step guide.

Sceel.io develops various native Android projects for its customers. Such projects demand large investments for development and support, because at least one iOS team is also required. Further it means more time is needed for communication, administration and testing. With Flutter, however, we were able to accelerate this process at a lower cost.

Nowadays switching themes is a popular feature in mobile apps. Users love to adjust interface to their needs, for example, turning on dark mode in the evening not to hurt their eyes.

In Flutter you can implement themes switching while saving the selected theme between app launches. Soon you will know how to use provider package for managing the state so that UI can react to theme changes. Moreover, you will implement saving of selected theme using shared_preferences. This package simply wraps NSUserDefaults on iOS and SharedPreferences on Android for storing the data.

 

 

STEP 1 Configure the project

Before you start building the UI, add a few dependencies. As I said, you have to add provider and shared_preferences packages.

 

pubspec.yaml
…
dependencies:
 flutter:
   sdk: flutter
 provider: ^3.1.0
 shared_preferences: ^0.5.3+4
…

 

 

 

STEP 2 Create custom themes

When the project is configured, your next move is to create some themes, so users can choose their favorite ones.

Flutter uses ThemeData – well – to store theme data. Let’s create 4 themes – 4 instances of ThemeData and store them in the map to have an easy way to access them. For keys use an enum.

 

app_themes.dart
import 'package:flutter/material.dart';

enum AppTheme {
 White, Dark, LightGreen, DarkGreen
}

/// Returns enum value name without enum class name.
String enumName(AppTheme anyEnum) {
 return anyEnum.toString().split('.')[1];
}

final appThemeData = {
 AppTheme.White : ThemeData(
   brightness: Brightness.light,
   primaryColor: Colors.white
 ),
 AppTheme.Dark : ThemeData(
   brightness: Brightness.dark,
   primaryColor: Colors.black
 ),
 AppTheme.LightGreen: ThemeData(
   brightness: Brightness.light,
   primaryColor: Colors.lightGreen
 ),
 AppTheme.DarkGreen: ThemeData(
   brightness: Brightness.dark,
   primaryColor: Colors.green
 )
};

 

 

STEP 3 Using provider

After custom themes are created you need a way to initialize and listen to theme changes. For this ChangeNotifier is used – a simple class that provides a change notification API.

 

theme_manager.dart
import 'package:flutter/material.dart';
import 'package:theme_app/theme/app_themes.dart';

class ThemeManager with ChangeNotifier {
 ThemeData _themeData;
 
 /// Use this method on UI to get selected theme.
 ThemeData get themeData {
   if (_themeData == null) {
     _themeData = appThemeData[AppTheme.White];
   }
   return _themeData;
 }

 /// Sets theme and notifies listeners about change. 
 setTheme(AppTheme theme) async {
   _themeData = appThemeData[theme];

   // Here we notify listeners that theme changed 
   // so UI have to be rebuild
   notifyListeners();
 }
}

 

 

As you can see ChangeNotifier is a mixin which has a very interesting method – notifyListeners().

Now, on to Provider – a package that helps with state management and dependency injection in Flutter. There are a few different kinds of providers for different types of objects. Here you need ChangeNotifierProvider. And guess what? It will work together with the previously created ThemeManager.

 


main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:theme_app/theme/theme_manager.dart';
import 'package:theme_app/ui/home.dart';

void main() => runApp(ThemeApp());

class ThemeApp extends StatelessWidget {

 @override
 Widget build(BuildContext context) {
   return ChangeNotifierProvider(
     //Here we provide our ThemeManager to child widget tree
     builder: (_) => ThemeManager(),

     //Consumer will call builder method each time ThemeManager
     //calls notifyListeners()
     child: Consumer(builder: (context, manager, _) {
       return MaterialApp(
           debugShowCheckedModeBanner: false,
           theme: manager.themeData,
           title: 'Theme app',
           home: Home());
     }),
   );
 }
}

 

 

ChangeNotifierProvider constructor has two essential parameters:

  1. Builder creates instance of ChangeNotifier (ThemeManager uses ChangeNotifier mixin in our case) and provides it to ChangeNotifierProvider descendants
  2. Consumer is kind of a listener of events emitted by ChangeNotifier (by calling notifyListeners()).

 

As it was mentioned before, ChangeNotifierProvider listens to a ChangeNotifier, exposes it to its descendants and rebuilds dependents whenever the ChangeNotifier.notifyListeners method is called.

You are using themeManager.themeData to set a theme in MaterialApp. Each time ThemeManager calls the notifyListeners method – the whole app is rebuilt with the new theme. For that you need to call setTheme method of ThemeManager. But first, let’s add a dummy Home to make UI look a bit more realistic.

 

home.dart
import 'package:flutter/material.dart';
import 'package:theme_app/ui/settings.dart';

class Home extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
       appBar: AppBar(
         title: Text('Home'),
         actions: [
           IconButton(
             icon: Icon(Icons.settings),
             onPressed: () {
               //Navigate to Settings screen
               Navigator.push(context, MaterialPageRoute(
                   builder: (context) => Settings()
               ));
             },
           )
         ],
       ),
       body: Center(
         child: Text(
           'Hello, Provider!',
           style: Theme.of(context).textTheme.headline,
         ),
       ));
 }
}

 

 

Settings screen is where the ThemeManager.setTheme method is called. You can access appThemeData and map it into ListView.

settings.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:theme_app/theme/app_themes.dart';
import 'package:theme_app/theme/theme_manager.dart';

class Settings extends StatelessWidget {

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text('Settings'),
     ),
     body: Padding(
       padding: const EdgeInsets.all(8.0),
       child: ListView.builder(
         itemCount: AppTheme.values.length,
         itemBuilder: (BuildContext context, int index) {
           // Get theme enum for the current item index
           final theme = AppTheme.values[index];
           return Card(
             // Style the item with corresponding theme color
             color: appThemeData[theme].primaryColor,
             child: ListTile(
               onTap: () {
                 // This will trigger notifyListeners and rebuild UI
                 // because of ChangeNotifierProvider in ThemeApp
                 Provider.of(context).setTheme(theme);
               },
               title: Text(
                 enumName(theme),
                 style: appThemeData[theme].textTheme.body1,
               ),
             ),
           );
         },
       ),
     ),
   );
 }
}

 

 

STEP 4 Persist user settings

Finally, you can test themes switching. But once the app is closed, the selected theme won’t be saved and default one will be chosen on the next launch. To fix this, use shared_preferences package to persist user’s preferred theme. Here you need a few lines of code in ThemeManager. First of all, load saved theme at the start and then each time saveTheme method is called, save selected theme in SharedPreferences.

theme_manager.dart
class ThemeManager with ChangeNotifier {
 ...
 final _kThemePreference = "theme_preference";

 ThemeManager() {
   // We load theme at the start
   _loadTheme();
 }

 void _loadTheme() {
   debugPrint("Entered loadTheme()");
   SharedPreferences.getInstance().then((prefs) {
     int preferredTheme = prefs.getInt(_kThemePreference) ?? 0;
     _themeData = appThemeData[AppTheme.values[preferredTheme]];
     // Once theme is loaded - notify listeners to update UI
     notifyListeners();
   });
 }

 ThemeData get themeData {
   ...
 }

 setTheme(AppTheme theme) async {
   ...

   // Save selected theme into SharedPreferences
   var prefs = await SharedPreferences.getInstance();
   prefs.setInt(_themePreference, AppTheme.values.indexOf(theme));
 }
}

 

 

As you can see in 4 simple steps with provider and some architecture decisions along the way, we managed to change themes while keeping code organized and clean.