Flutter Dynamic Themes with cubit and hydrated cubit

Flutter Dynamic Themes with cubit and hydrated cubit

Learn how to change themes in flutter dynamically with blog cubit and hydrated cubit

·

7 min read

Hello guys, In this tutorial, we will gonna learn, how to change themes in flutter apps dynamically with the help of cubit and hydrated cubit.

You can get the full source code here

0: Create New Flutter Project

We will start by creating a brand new flutter project. Make sure you've installed the flutter SDK & android studio from the official website if not then please install those things first from the link below.

After you've installed everything on your machine now it's time to run the flutter command and wait for it to complete.

flutter create dynamic_theme_demo

1: Setup ( Installing Dependencies )

Now let's open the project folder in VSCode or any IDE of your choice and install the required dependencies we will be needing in our project.

Go to the pubspec.yaml file on the root directory of the project and paste these packages to the dependencies list and after that run theflutter pub get command if required in VSCode it will be done automatically when you will save the file.

  bloc: ^7.2.1
  flutter_bloc: ^7.3.1
  google_fonts: ^2.1.0
  hydrated_bloc: ^7.1.0
  path_provider: ^2.0.5

2: Making Themes Presets

After installing all the dependencies, now it's time to make some theme presets.

Create new file named themes.dart inside themes folder under lib folder lib/themes/themes.dart

There are many ways to achieve this but we will take the path which makes sense to us. We will create an enum to represent different theme presets. I've used 6 themes presets but you can use as many as you want.

enum AppTheme {
  redDark,
  redLight,
  blueLight,
  blueDark,
  greenDark,
  greenLight,
}

Below that, we will create a Map<AppTheme, ThemeData>, so that we can map the correct theme preset using the enum we've created above.


final Map<AppTheme, ThemeData> appThemeData = {
  AppTheme.redDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.red,
    primarySwatch: Colors.red,
    textTheme: GoogleFonts.dancingScriptTextTheme(),
  ),
  AppTheme.redLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.red,
    primarySwatch: Colors.red,
    textTheme: GoogleFonts.oswaldTextTheme(),
  ),
  AppTheme.blueLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.indigo,
    primarySwatch: Colors.indigo,
    textTheme: GoogleFonts.ubuntuTextTheme(),
  ),
  AppTheme.blueDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.indigo,
    primarySwatch: Colors.indigo,
    textTheme: GoogleFonts.irishGroverTextTheme(),
  ),
  AppTheme.greenDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.green,
    primarySwatch: Colors.green,
    textTheme: GoogleFonts.architectsDaughterTextTheme(),
  ),
  AppTheme.greenLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.green,
    primarySwatch: Colors.green,
    textTheme: GoogleFonts.permanentMarkerTextTheme(),
  ),
};

With that done, your themes.dart file must look like this.


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

enum AppTheme {
  redDark,
  redLight,
  blueLight,
  blueDark,
  greenDark,
  greenLight,
}

final Map<AppTheme, ThemeData> appThemeData = {
  AppTheme.redDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.red,
    primarySwatch: Colors.red,
    textTheme: GoogleFonts.dancingScriptTextTheme(),
  ),
  AppTheme.redLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.red,
    primarySwatch: Colors.red,
    textTheme: GoogleFonts.oswaldTextTheme(),
  ),
  AppTheme.blueLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.indigo,
    primarySwatch: Colors.indigo,
    textTheme: GoogleFonts.ubuntuTextTheme(),
  ),
  AppTheme.blueDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.indigo,
    primarySwatch: Colors.indigo,
    textTheme: GoogleFonts.irishGroverTextTheme(),
  ),
  AppTheme.greenDark: ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.green,
    primarySwatch: Colors.green,
    textTheme: GoogleFonts.architectsDaughterTextTheme(),
  ),
  AppTheme.greenLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.green,
    primarySwatch: Colors.green,
    textTheme: GoogleFonts.permanentMarkerTextTheme(),
  ),
};

3: Creating New Cubit

Using the bloc extension create a new cubit in the lib directory. In case you don't know what cubit is? I'd suggest you get familiar with bloc library. You can also get great videos on youtube on bloc & cubit.

So, with that out of the way, we will create a new cubit namely dynamic_theme with the help of bloc extension. That will create two files under the cubit folder.

  • dynamic_theme_cubit.dart
  • dynamic_theme_state.dart

Dynamic Theme State

This is what it will look like at first

part of 'dynamic_theme_cubit.dart';

@immutable
abstract class DynamicThemeState {}

class DynamicThemeInitial extends DynamicThemeState {}

As we will be dealing with a single state, we don't need to have multiple State classes. And we also don't need the abstract class. So, delete unnecessary classes and also abstract keywords from the main state class.

After performing those deletions, your file should look like this.

part of 'dynamic_theme_cubit.dart';

@immutable
class DynamicThemeState {}

After this, we will create a final field of type AppTheme (enum) and also create a constructor to initialize that field.

part of 'dynamic_theme_cubit.dart';

@immutable
class DynamicThemeState {
  final AppTheme theme;
  const DynamicThemeState({
    required this.theme,
  });
}

With that, we are done with the state file. yeah! that's that simple.

Dynamic State Cubit

Now the cubit file will look something like this at first.

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';

part 'dynamic_theme_state.dart';

class DynamicThemeCubit extends Cubit<DynamicThemeState> {
  DynamicThemeCubit() : super(DynamicThemeInitial());
}

Now the real fun begins. Instead of extending Cubit Abstract Class we will extend HydratedCubit Class. Then we need to override two methods toJson and fromJson. This is because Hydrated Cubit will persist the state as JSON in the device storage media. So we have to implement the state conversion ourselves. And also we will provide the initial state for the cubit. I'll pick the redDark theme as a initial state.

import 'package:flutter_dynamic_theme/themes/themes.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';

part 'dynamic_theme_state.dart';

class DynamicThemeCubit extends HydratedCubit<DynamicThemeState> {
  DynamicThemeCubit()
      : super(
          const DynamicThemeState(theme: AppTheme.redDark),
        );

  @override
  DynamicThemeState? fromJson(Map<String, dynamic> json) {
    // TODO: implement fromJson
    throw UnimplementedError();
  }

  @override
  Map<String, dynamic>? toJson(DynamicThemeState state) {
    // TODO: implement toJson
    throw UnimplementedError();
  }
}

Now Lets implement the toJson function. This function takes State as a parameter which is basically AppTheme (enum), We will convert the enum value to string and create a map with the key theme and return it.

@override
  Map<String, dynamic>? toJson(DynamicThemeState state) {
    final theme = {
      'theme': state.theme.toString(),
    };

    return theme;
  }

With that done, now we will implement the fromJSON method, this function takes Map json as a parameter. we will extract the theme value out of that map and assign it to the theme variable.

After that, we will create an AppTheme variable, and using the firstWhere() function to the values property will check for the matching string and assign the appropriate value to it.

  @override
  DynamicThemeState? fromJson(Map<String, dynamic> json) {
    final theme = json['theme'];

    AppTheme currentTheme =
        AppTheme.values.firstWhere((e) => e.toString() == theme);

    return DynamicThemeState(theme: currentTheme);
  }

With that done, now finally we will create a function that changes the theme. It will take AppTheme as a parameter and emit a new state including that AppTheme variable.

  void changeTheme({required AppTheme theme}) =>
      emit(DynamicThemeState(theme: theme));

With everything done, your dynamic_theme_cubit.dart file will look something like this.

import 'package:flutter_dynamic_theme/themes/themes.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';

part 'dynamic_theme_state.dart';

class DynamicThemeCubit extends HydratedCubit<DynamicThemeState> {
  DynamicThemeCubit()
      : super(
          const DynamicThemeState(theme: AppTheme.redDark),
        );

  @override
  DynamicThemeState? fromJson(Map<String, dynamic> json) {
    final theme = json['theme'];

    AppTheme currentTheme =
        AppTheme.values.firstWhere((e) => e.toString() == theme);

    return DynamicThemeState(theme: currentTheme);
  }

  @override
  Map<String, dynamic>? toJson(DynamicThemeState state) {
    final theme = {
      'theme': state.theme.toString(),
    };

    return theme;
  }

  void changeTheme({required AppTheme theme}) =>
      emit(DynamicThemeState(theme: theme));
}

4: Creating Pages

we will take the bottom-up approach here. There will be two pages altogether

  • Home Page
  • Settings Page

Home Page will contain some text to demonstrate the Theme Change.

Settings Page will contain all the themes as a list which when pressed will trigger a function that will change the app theme.

Settings Page

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dynamic_theme/cubit/dynamic_theme_cubit.dart';
import 'package:flutter_dynamic_theme/themes/themes.dart';

class SettingPage extends StatelessWidget {
  const SettingPage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('App Settings'),
        backgroundColor: Theme.of(context).primaryColor,
      ),
      body: BlocBuilder<DynamicThemeCubit, DynamicThemeState>(
        builder: (context, state) {
          return ListView.builder(
            itemCount: AppTheme.values.length,
            itemBuilder: (context, index) {
              final itemAppTheme = AppTheme.values[index];
              return Card(
                elevation: 5,
                color: appThemeData[itemAppTheme]!.primaryColor,
                child: ListTile(
                  onTap: () => context
                      .read<DynamicThemeCubit>()
                      .changeTheme(theme: itemAppTheme),
                  title: Text(
                    itemAppTheme.toString(),
                    style: appThemeData[itemAppTheme]!.textTheme.bodyText1,
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Home Page

import 'package:flutter/material.dart';
import 'package:flutter_dynamic_theme/pages/settings_page.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Theme Demo'),
        backgroundColor: Theme.of(context).primaryColor,
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Home Page',
              style: Theme.of(context).textTheme.headline3!.copyWith(
                    color: Theme.of(context).primaryColor,
                  ),
            ),
            Text(
              'Dynamic Theme Demo With Hydrated Cubit',
              style: Theme.of(context).textTheme.subtitle1!.copyWith(
                    color: Theme.of(context).primaryColor,
                  ),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Theme.of(context).primaryColor,
        onPressed: () => Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => const SettingPage(),
        )),
        child: const Icon(Icons.settings),
      ),
    );
  }
}

Root Widget

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dynamic_theme/cubit/dynamic_theme_cubit.dart';
import 'package:flutter_dynamic_theme/pages/home_page.dart';
import 'package:flutter_dynamic_theme/themes/themes.dart';

class RootWidget extends StatelessWidget {
  const RootWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider<DynamicThemeCubit>(
      create: (context) => DynamicThemeCubit(),
      child: BlocBuilder<DynamicThemeCubit, DynamicThemeState>(
        builder: (context, state) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            theme: appThemeData[state.theme],
            home: const HomePage(),
            initialRoute: '/',
          );
        },
      ),
    );
  }
}

6: main.dart file

import 'package:flutter/material.dart';
import 'package:flutter_dynamic_theme/pages/root_widget.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  HydratedBloc.storage = await HydratedStorage.build(
    storageDirectory: await getTemporaryDirectory(),
  );
  runApp(const RootWidget());
}

Did you find this article valuable?

Support Apedu.co by becoming a sponsor. Any amount is appreciated!