Flutter Hydrated
Block Pattern

Part 8 – Flutter BLoC Tutorial: Advanced Concepts (Hydrated BLoC, Debounce, Error Handling)

📘 Chapter 8: Advanced BLoC Concepts

This chapter dives into advanced Flutter BLoC techniques that elevate the reliability and responsiveness of your app. We’ll explore three major patterns:


🔍 Key Concepts and Terminologies

Term Description
Hydrated BLoC Automatically persists the BLoC state on local storage and restores it on app restart. Ideal for counters, filters, settings, etc.
Event Transformer (Debounce) Controls how frequently events are processed. Debounce ensures only the latest event after a pause is handled.
BlocObserver / onError Catches errors globally from all BLoC and Cubit classes for unified logging or analytics.

💡 Real-World Example: User Search with Persistent Counter

We’ll implement a working Flutter app with:

  • A Hydrated BLoC counter that survives app restarts.
  • A debounced search field that fetches users from the JSONPlaceholder API.
  • Centralized error logging with SimpleBlocObserver.

📦 Dependencies (pubspec.yaml)

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  hydrated_bloc: ^9.1.0
  path_provider: ^2.0.0
  http: ^0.13.6
  rxdart: ^0.27.7

🛠️ main.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';

import 'bloc/counter_cubit.dart';
import 'bloc/search_bloc.dart';
import 'bloc/simple_bloc_observer.dart';
import 'home_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final storage = await HydratedStorage.build(
    storageDirectory: await getApplicationDocumentsDirectory(),
  );

  HydratedBloc.storage = storage;
  Bloc.observer = SimpleBlocObserver();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (_) => CounterCubit()),
        BlocProvider(create: (_) => SearchBloc()),
      ],
      child: MaterialApp(
        title: 'Advanced BLoC Demo',
        home: HomePage(),
      ),
    );
  }
}

💾 counter_cubit.dart (Hydrated BLoC)

import 'package:hydrated_bloc/hydrated_bloc.dart';

class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);

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

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

⏳ search_bloc.dart (With Debounce & API)

import 'dart:convert';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
import 'package:rxdart/rxdart.dart';

abstract class SearchEvent {
  final String query;
  const SearchEvent(this.query);
}

class QueryChanged extends SearchEvent {
  QueryChanged(String query) : super(query);
}

class SearchBloc extends Bloc<SearchEvent, List<String>> {
  SearchBloc() : super([]) {
    on<QueryChanged>(
      _onQueryChanged,
      transformer: debounce(const Duration(milliseconds: 400)),
    );
  }

  Future<void> _onQueryChanged(QueryChanged event, Emitter<List<String>> emit) async {
    try {
      final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
      if (response.statusCode == 200) {
        final List users = json.decode(response.body);
        final results = users
            .where((user) =>
                user['name'].toLowerCase().contains(event.query.toLowerCase()))
            .map<String>((user) => user['name'] as String)
            .toList();
        emit(results);
      } else {
        emit(['Failed to fetch users']);
      }
    } catch (e) {
      emit(['Error: $e']);
    }
  }

  EventTransformer<E> debounce<E>(Duration duration) {
    return (events, mapper) => events.debounceTime(duration).switchMap(mapper);
  }
}

❗ simple_bloc_observer.dart (Error Handling)

import 'package:flutter_bloc/flutter_bloc.dart';

class SimpleBlocObserver extends BlocObserver {
  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('⚠️ Error in ${bloc.runtimeType}: $error');
    super.onError(bloc, error, stackTrace);
  }
}

🖼️ home_page.dart (UI)

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/counter_cubit.dart';
import 'bloc/search_bloc.dart';

class HomePage extends StatelessWidget {
  final TextEditingController controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterCubit>().state;
    final searchResults = context.watch<SearchBloc>().state;

    return Scaffold(
      appBar: AppBar(title: Text('Advanced BLoC Concepts')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Text('Counter: $counter', style: TextStyle(fontSize: 24)),
            ElevatedButton(
              onPressed: () => context.read<CounterCubit>().increment(),
              child: Text('Increment Counter'),
            ),
            SizedBox(height: 30),
            TextField(
              controller: controller,
              onChanged: (query) =>
                  context.read<SearchBloc>().add(QueryChanged(query)),
              decoration: InputDecoration(
                labelText: 'Search Users...',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 20),
            Expanded(
              child: ListView.builder(
                itemCount: searchResults.length,
                itemBuilder: (_, index) => ListTile(
                  title: Text(searchResults[index]),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

✅ Summary

Feature Purpose
Hydrated BLoC Saves and restores state across app restarts
Debounce Prevents rapid API calls while typing
Error Handling Logs BLoC errors globally in one place

Leave a Reply

Your email address will not be published. Required fields are marked *