Change the App theme using Flutter Bloc (Hydrated Bloc)

Azhar Ali
4 min readJan 24, 2024

--

What is Hydrated Bloc?

Hydrated Bloc is an extension of the popular Flutter state management library bloc. It adds built-in functionality to persist the state of your Blocs in local storage, even when the app is closed or restarted. This can be very beneficial for saving user preferences, application settings, or any other data that you want to remain accessible across app sessions.

If you’re already using Bloc in your project, consider transitioning to Hydrated Bloc. While Hydrated Bloc does require the flutter_bloc package, it offers the additional benefit of persisting your state across app restarts.

Let’s start with the implementation of the Hydrated bloc

Add these packages

flutter_bloc: ^8.1.3
hydrated_bloc: ^9.1.3
path_provider: ^2.1.2

path_provider is a Flutter plugin that provides a simple way to access platform-specific locations for storing persistent data. It helps Flutter applications retrieve directories such as documents, caches, and temporary storage on both Android and iOS devices. This plugin abstracts the platform-specific details, allowing developers to access file system paths in a unified manner across different platforms.

Creating a cubit for a Hydrated bloc

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

class ThemeCubit extends HydratedCubit<ThemeMode> {

// set ThemeMode.System as default option which would be used for initial case
ThemeCubit() : super(ThemeMode.system);


void updateTheme(ThemeMode theme) => emit(theme);


@override
ThemeMode? fromJson(Map<String, dynamic> json) {
return ThemeMode.values[json['theme'] as int];
}

@override
Map<String, dynamic>? toJson(ThemeMode mode) {
return {'theme': mode.index};
}

}

ThemeMode is an enum, and in programming languages, enums are typically treated as integers. In this case, there are three potential values defined in the Dart material library: dark, light, and system. Therefore, when working with ThemeMode, you are essentially dealing with an index value corresponding to one of these three options.

Initialization is necessary in the main method for Hydrated BLoC to reinitialize stored data.

import 'package:path_provider/path_provider.dart';

Future<void> main() async {
// initialize hydrated bloc
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getApplicationDocumentsDirectory(),
);

runApp(const MyApp());
}

Hydrated BLoC provides caching capabilities on the web, and it’s essential to clearly define this during initialization using HydratedStorage.webStorageDirectory. Additionally, the getApplicationDocumentDirectory functions will facilitate access to the file system on Android and iOS.

Important Note: When working with the web and implementing theme changes with local persistence, it’s crucial to specify the localhost port. During development and testing, Flutter uses different ports on each run. Failing to specify the port can lead to the loss of stored BLoC data, emphasizing the need for careful consideration until the application goes live.

How to specify a port in Android Studio?

How to specify a port in VS Code?

If you’re working with a terminal

flutter run --web-port=60979

MaterialApp

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

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => ThemeCubit()),
],
child: BlocBuilder<ThemeCubit, ThemeMode>(
builder: (context, ThemeMode mode) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Theme change',
themeMode: mode,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
home: MyHomePage(),
);
},
),
);
}
}

Add ThemeCubit as MultiBlocProvider to access it globally and use the BlocBuilder for ThemeCubit to list the changes of the app theme

// theme mode may the (dark, light, system)
themeMode: mode,
// add dark and light theme in Material App
theme: AppTheme.light,
darkTheme: AppTheme.dark,

Change theme class

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

final List<(String, ThemeMode)> _themes = const [
('Dark', ThemeMode.dark),
('Light', ThemeMode.light),
('System', ThemeMode.system),
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Change Theme')),
body: Center(
child: BlocBuilder<ThemeCubit, ThemeMode>(
builder: (context, selectedTheme) {
return Column(
children: List.generate(
_themes.length,
(index) {
final String label = _themes[index].$1;
final ThemeMode theme = _themes[index].$2;
return ListTile(
title: Text(label),
onTap: () => context.read<ThemeCubit>().updateTheme(theme),
trailing: selectedTheme == theme ? const Icon(Icons.check) : null,
);
},
),
);
},
),
),
);
}
}

Create a list containing labels and corresponding theme modes. Display the labels to end users while utilizing the associated theme mode to dynamically update the app’s theme. This approach ensures a user-friendly experience with the ability to seamlessly switch between different themes.

  final List<(String, ThemeMode)> _themes = const [
('Dark', ThemeMode.dark),
('Light', ThemeMode.light),
('System', ThemeMode.system),
];

Consider defining a model to efficiently manage the list of themes. If you wish to incorporate additional features, such as icons for each theme, create a dedicated model for the theme list and adapt it according to your coding preferences.

To enhance organization, encapsulate the themes in a separate class as static getters, enabling easy access in the main class.

import 'package:flutter/material.dart';

class AppTheme {
static ThemeData get dark => ThemeData(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: Colors.green,
),
);

static ThemeData get light => ThemeData(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.light,
seedColor: Colors.blue,
),
);
}

I’ve included fundamental options for both dark and light themes, and these can be expanded and customized based on the specific requirements of your project.

We’re done now with the dark and light theme-switching feature.

In conclusion, the implementation of a dynamic dark-light theme-switching feature not only enhances user experience by providing visual customization but also underscores the flexibility and adaptability of the application. Users can now enjoy a personalized interface that aligns with their preferences, contributing to an overall engaging and user-centric application environment.

The code is also available from this GitHub repository.

Here is a details article related to the theme and I hope it will be helpful for you.

--

--