Switch themes easily in Flutter using Bloc

Switch themes easily in Flutter using Bloc

As a user of any application, We try to switch between the themes and choose which the theme that we like the most and as a mobile app developer, We should give our application users a flexibility to switch between themes which improves user experience.

Here, We're going to learn theme switching using Bloc as state management.

First, create a new app using flutter create appname and open it in IDE of your preference.

Well, in your pubspec.yaml file add

  shared_preferences: ^2.0.15
  flutter_bloc: ^8.1.1

Flutter bloc is for state management while shared preferences is for saving theme locally so that while opening app next time, we can check which theme is activated and then set the theme.

Add a new extension bloc from store in your IDE and then right click the lib folder and then click option new bloc and then give bloc a name of theme. A new folder with name theme_bloc will be generated.

Now remove theme_state file from the folder and you'll get this folder file structure and also a few errors. Screen Shot 2022-10-08 at 19.38.05.png

Now, go to theme_bloc.dart file and you'll see errors in ThemeState as theme state is not recognized since we've removed the file. Replace that ThemeState by ThemeData. Import necessary imports and remove unrecognized and unnecessary imports. You'll get this:

      class ThemeBloc extends Bloc<ThemeEvent, ThemeData> {
        ThemeBloc() : super(ThemeData.light()) {
         on<ThemeEvent>((event, emit){});
        }

Now, our ThemeBloc's state will be of ThemeData type i.e. when we emit state in this bloc, we'll have to emit state in for ThemeData.light(), ThemeData.dark(), etc.

Now in theme_event.dart file add two events. ThemeSwitchEvent and InitialThemeSetEvent extending ThemeEvent like this:

  part of 'theme_bloc.dart';

  @immutable
  abstract class ThemeEvent {}

  class InitialThemeSetEvent extends ThemeEvent {}

  class ThemeSwitchEvent extends ThemeEvent {}

Now, We're going to handle the logic in theme_bloc.dart file as per our event. Firstly, let's work on ThemeSwitchEvent. The main use case is: When a user clicks a switch that is used to change theme, we need to add this event. And when this event is triggered, we need to check what theme is currently activated and we need to change the theme to dark if light is activated currently and to light if dark is activated and We'll also need to add certain value to shared preferences in such a way that we can identify what theme is currently activated in the app so that when user restarts the app, we can activate that theme initially. This initial theme setting logic is handled by the InitialThemeSetEvent.

Now, create a file theme_helper.dart just to add two functions to set bool value in shared preferences with key "is_dark" that keeps a value true if theme is dark and false if theme is true.

In theme_helper.dart file add this two functions:


  Future<bool> isDark() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getBool("is_dark") ?? false;
  }

  Future<void> setTheme(bool isDark) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setBool("is_dark", !isDark);
  }

The function isDark() which is called while opening the app returns true if the value inside the key "is_dark" is true and false if value is false or null. The setTheme function is triggered once the theme is updated after clicking the switch i.e. on ThemeSwitchEvent.

Now, update your theme_bloc.dart file like this to update theme.

  class ThemeBloc extends Bloc<ThemeEvent, ThemeData> {
    ThemeBloc() : super(ThemeData.light()) {
      //when app is started
      on<InitialThemeSetEvent>((event, emit) async {
        final bool hasDarkTheme = await isDark();
        if (hasDarkTheme) {
          emit(ThemeData.dark());
        } else {
          emit(ThemeData.light());
        }
      });

      //while switch is clicked
      on<ThemeSwitchEvent>((event, emit) {
        final isDark = state == ThemeData.dark();
        emit(isDark ? ThemeData.light() : ThemeData.dark());
        setTheme(isDark);
      });
    }
  }

Now the logic is handled here in theme_bloc.dart. Now you need to call the events in the app and it is necessary to understand where to call the event.

The theme your app is currently using can be switched from MaterialApp where we give theme property. So we need to provide MaterialApp with our state as our state is currently activated theme. Now, Wrap MaterialApp with BlocBuilder of ThemeBloc after clicking (Control + .)/(Command + .). In theme key of MaterialApp give state as value as in our ThemeBloc state means ThemeData. The result will be like this:

  class MyApp extends StatelessWidget {
    const MyApp({super.key});

    @override
    Widget build(BuildContext context) {
      return BlocBuilder<ThemeBloc, ThemeData>(
        builder: (context, state) {
          return MaterialApp(
            theme: state,
            debugShowCheckedModeBanner: false,
            home: const HomePage(),
          );
        },
      );
    }
  }

Create home.dart inside lib folder and in the content of HomePage add this content:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:theme_switcher/bloc/theme_bloc.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          BlocBuilder<ThemeBloc, ThemeData>(
            builder: (context, themeData) {
              return CupertinoSwitch(
                  value: themeData == ThemeData.dark(),
                  onChanged: (bool val) {
                    BlocProvider.of<ThemeBloc>(context).add(ThemeSwitchEvent());
                  });
            },
          ),
        ],
      ),
      body: const Center(child: Text("Theme changing app")),
    );
  }
}

The code up to here is fine and we need to give a final touch to the code. If we run the above code we'll run into an exception of Provider not found or something. This is because we've not provided ThemeBloc to our App i.e. we need to provide our app with ThemeBloc above the place in widget tree where it is referenced.

Now, Inside runApp method where you're calling MyApp() i.e. the root of your app where you've declared MaterialApp, you need to wrap your MyApp() like this:

  BlocProvider(
      create: (context) => ThemeBloc()
      child: const MyApp(),
    )

If we do this, our app will run fine. Theme will also switch and the value is also set in shared preferences. Now, set the theme to dark and restart the app once, you'll see the theme is light even though we set the theme to dark and also set the value in shared preferences. This is because we didn't call InitialThemeSetEvent initially while our app's starting which led to Light theme by default as we kept light theme as default in theme bloc state. Now to call this, Update the above piece of code like this:

  BlocProvider(
      create: (context) => ThemeBloc()..add(InitialThemeSetEvent())
      child: const MyApp(),
    )

This'll provide theme bloc to the app and also call InitialThemeSetEvent simultaneously. Now, We're done.

The full source code is at: github.com/ankeet7x/theme_switch_using_bloc