Persisting State in Flutter Using Hydrated BLoC with Freezed

Mohammed shamseer pv
4 min readNov 19, 2024

--

In this tutorial, we’ll build a Flutter app that uses Hydrated BLoC with the Freezed package to persist the state of a counter and theme brightness settings.

Setting Up Your Project

Add the following dependencies to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.0
hydrated_bloc: ^9.0.0
path_provider: ^2.0.0
freezed_annotation: ^3.1.0

dev_dependencies:
build_runner: ^2.4.0
freezed: ^3.1.0

Run flutter pub get to install these dependencies.

Generating Freezed Classes

The Freezed package will help define immutable states and events for our BrightnessBloc and CounterBloc.

1. Define Events and States for BrightnessBloc

Create a file brightness_bloc.dart:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

part 'brightness_bloc.freezed.dart';
part 'brightness_bloc.g.dart';

@freezed
class BrightnessEvent with _$BrightnessEvent {
const factory BrightnessEvent.toggle() = BrightnessToggle;
}

@freezed
class BrightnessState with _$BrightnessState {
const factory BrightnessState({required bool isDarkMode}) = _BrightnessState;

factory BrightnessState.fromJson(Map<String, dynamic> json) =>
_$BrightnessStateFromJson(json);
}

class BrightnessBloc extends HydratedBloc<BrightnessEvent, BrightnessState> {
BrightnessBloc() : super(const BrightnessState(isDarkMode: false)) {
on<BrightnessToggle>((event, emit) {
emit(state.copyWith(isDarkMode: !state.isDarkMode));
});
}

@override
BrightnessState? fromJson(Map<String, dynamic> json) =>
BrightnessState.fromJson(json);

@override
Map<String, dynamic>? toJson(BrightnessState state) => state.toJson();
}

2. Define Events and States for CounterBloc

Create a file counter_bloc.dart:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

part 'counter_bloc.freezed.dart';
part 'counter_bloc.g.dart';

@freezed
class CounterEvent with _$CounterEvent {
const factory CounterEvent.increment() = CounterIncrement;
const factory CounterEvent.decrement() = CounterDecrement;
}

@freezed
class CounterState with _$CounterState {
const factory CounterState({required int value}) = _CounterState;

factory CounterState.fromJson(Map<String, dynamic> json) =>
_$CounterStateFromJson(json);
}

class CounterBloc extends HydratedBloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<CounterIncrement>((event, emit) {
emit(state.copyWith(value: state.value + 1));
});
on<CounterDecrement>((event, emit) {
emit(state.copyWith(value: state.value - 1));
});
}

@override
CounterState? fromJson(Map<String, dynamic> json) =>
CounterState.fromJson(json);

@override
Map<String, dynamic>? toJson(CounterState state) => state.toJson();
}

Run flutter pub run build_runner build to generate the *.freezed.dart and *.g.dart files.

3. App Initialization

Update your main.dart:

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

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

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => BrightnessBloc()),
BlocProvider(create: (_) => CounterBloc()),
],
child: const AppView(),
);
}
}

4. AppView

The AppView adapts its theme based on the BrightnessBloc state:

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

@override
Widget build(BuildContext context) {
return BlocBuilder<BrightnessBloc, BrightnessState>(
builder: (context, state) {
return MaterialApp(
theme: ThemeData(
brightness: state.isDarkMode ? Brightness.dark : Brightness.light,
),
home: const CounterPage(),
);
},
);
}
}

5. CounterView

The CounterView is updated to use the new CounterBloc:

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

@override
Widget build(BuildContext context) {
return const CounterView();
}
}

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

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;

return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('${state.value}', style: textTheme.displayMedium);
},
),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () =>
context.read<BrightnessBloc>().add(const BrightnessToggle()),
child: const Icon(Icons.brightness_6),
),
const SizedBox(height: 4),
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(const CounterIncrement()),
child: const Icon(Icons.add),
),
const SizedBox(height: 4),
FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(const CounterDecrement()),
child: const Icon(Icons.remove),
),
const SizedBox(height: 4),
FloatingActionButton(
onPressed: () => HydratedBloc.storage.clear(),
child: const Icon(Icons.delete_forever),
),
],
),
);
}
}

Cubit-Full Code

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 getApplicationDocumentsDirectory(),
);
runApp(const App());
}


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

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BrightnessCubit(),
child: const AppView(),
);
}
}

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

@override
Widget build(BuildContext context) {
return BlocBuilder<BrightnessCubit, Brightness>(
builder: (context, brightness) {
return MaterialApp(
theme: ThemeData(brightness: brightness),
home: const CounterPage(),
);
},
);
}
}

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

@override
Widget build(BuildContext context) {
return BlocProvider<CounterBloc>(
create: (_) => CounterBloc(),
child: const CounterView(),
);
}
}

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

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, state) {
return Text('$state', style: textTheme.displayMedium);
},
),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.brightness_6),
onPressed: () => context.read<BrightnessCubit>().toggleBrightness(),
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
context.read<CounterBloc>().add(CounterIncrementPressed());
},
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () {
context.read<CounterBloc>().add(CounterDecrementPressed());
},
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.delete_forever),
onPressed: () => HydratedBloc.storage.clear(),
),
],
),
);
}
}

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends HydratedBloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}

@override
int fromJson(Map<String, dynamic> json) => json['value'] as int;

@override
Map<String, int> toJson(int state) => {'value': state};
}

class BrightnessCubit extends HydratedCubit<Brightness> {
BrightnessCubit() : super(Brightness.light);

void toggleBrightness() {
emit(state == Brightness.light ? Brightness.dark : Brightness.light);
}

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

@override
Map<String, dynamic> toJson(Brightness state) {
return <String, int>{'brightness': state.index};
}
}

Final Thoughts

With Freezed, defining states and events becomes concise, and using Hydrated BLoC ensures persistence with minimal boilerplate. Implement these techniques in your projects to enhance user experience.

--

--

Mohammed shamseer pv
Mohammed shamseer pv

Written by Mohammed shamseer pv

skilled in Flutter, Node.js, Python, and Arduino, passionate about AI and creating innovative solutions. Active in tech community projects.

No responses yet