BLoC Local Storage: Persisting Data with Hive, SharedPreferences & SQLite
Last Sync: Today
flutter
BLoC Local Storage: Persisting Data with Hive, SharedPreferences & SQLite
Introduction
Local storage is essential for building responsive, offline‑capable Flutter apps. When combined with BLoC, it provides a clean separation between data sources and UI logic. This guide covers how to integrate popular local storage solutions—Hive, SharedPreferences, and SQLite—with BLoC using the repository pattern. You'll learn caching strategies, offline support, and best practices to keep your app fast and reliable.
Why Use Local Storage with BLoC?
Offline Access – Serve cached data when the network is unavailable.
Performance – Reduce network calls by storing frequently used data locally.
User Preferences – Save settings, themes, and user sessions.
Data Persistence – Keep state across app restarts.
Separation of Concerns – Keep storage logic in repositories, not inside BLoCs.
Choosing the Right Storage
Storage
Use Case
Key Features
SharedPreferences
Simple key‑value, small data (settings, flags)
Easy API, synchronous, no complex types
Hive
Fast key‑value, complex objects, large datasets
Type‑safe, no SQL, supports encryption
SQLite
Relational data, complex queries, large collections
Full SQL, migrations, ACID compliance
Repository Pattern with BLoC
The repository pattern acts as a single source of truth, abstracting data sources (local/remote). Your BLoC interacts only with the repository, which decides whether to fetch from cache or network.
DARTRead-only
1
classUserRepository{
final LocalDataSource localDataSource;
final RemoteDataSource remoteDataSource;UserRepository(this.localDataSource,this.remoteDataSource);
Future<User>getUser(String id) async {// Try local first
final localUser =await localDataSource.getUser(id);if(localUser !=null)return localUser;// Fallback to network
final remoteUser =await remoteDataSource.getUser(id);await localDataSource.saveUser(remoteUser);return remoteUser;}}// BLoC uses the repositoryclassUserBlocextendsBloc<UserEvent, UserState>{
final UserRepository repository;UserBloc(this.repository):super(UserInitial());
on<LoadUser>((event, emit) async {emit(UserLoading());try{
final user =await repository.getUser(event.id);emit(UserLoaded(user));}catch(e){emit(UserError(e.toString()));}});}
Integrating SharedPreferences with BLoC
SharedPreferences is ideal for storing simple settings. Create a service that wraps SharedPreferences and inject it into your BLoC or repository.
DARTRead-only
1
classPreferencesService{staticconst String _themeKey ='theme_mode';
final SharedPreferences _prefs;PreferencesService(this._prefs);
ThemeMode getThemeMode(){
final index = _prefs.getInt(_themeKey)??0;return ThemeMode.values[index];}
Future<void>setThemeMode(ThemeMode mode) async {await _prefs.setInt(_themeKey, mode.index);}}classSettingsBlocextendsBloc<SettingsEvent, SettingsState>{
final PreferencesService prefs;SettingsBloc(this.prefs):super(SettingsInitial()){
on<LoadSettings>((event, emit){emit(SettingsLoaded(prefs.getThemeMode()));});
on<ThemeChanged>((event, emit) async {await prefs.setThemeMode(event.mode);emit(SettingsLoaded(event.mode));});}}
Integrating Hive with BLoC
Hive is a fast, NoSQL database perfect for storing complex objects. Define adapters and open boxes before using them.
For relational data, SQLite is a solid choice. Use the sqflite package and create a database helper.
DARTRead-only
1
classDatabaseHelper{static Database? _database;staticconst String _dbName ='app.db';staticconst int _version =1;
Future<Database>get database async {if(_database !=null)return _database!;
_database =await_initDatabase();return _database!;}
Future<Database>_initDatabase() async {
final path =awaitgetDatabasesPath();returnopenDatabase(join(path, _dbName),onCreate:(db, version){return db.execute('CREATE TABLE todos(id INTEGER PRIMARY KEY, title TEXT, completed INTEGER)',);},version: _version,);}}classTodoRepository{
final DatabaseHelper dbHelper;TodoRepository(this.dbHelper);
Future<List<Todo>>getTodos() async {
final db =await dbHelper.database;
final List<Map<String, dynamic>> maps =await db.query('todos');return maps.map((map)=> Todo.fromMap(map)).toList();}
Future<void>addTodo(Todo todo) async {
final db =await dbHelper.database;await db.insert('todos', todo.toMap());}}
Caching Strategies
Cache‑First – Always serve from cache; update cache from network in background.
Network‑First – Try network; fallback to cache if offline or error.
Cache‑Then‑Network – Show cached data immediately, then refresh from network.
Time‑Based Expiry – Invalidate cache after a certain duration.
DARTRead-only
1
classCacheService{
final Box<dynamic> cacheBox;
final Duration ttl;CacheService(this.cacheBox,this.ttl);
Future<T?> get<T>(String key) async {
final entry = cacheBox.get(key);if(entry ==null)returnnull;
final timestamp = cacheBox.get('${key}_timestamp');if(timestamp !=null&& DateTime.now().difference(DateTime.parse(timestamp))> ttl){await cacheBox.delete(key);returnnull;}return entry asT;}
Future<void>set(String key, dynamic value) async {await cacheBox.put(key, value);await cacheBox.put('${key}_timestamp', DateTime.now().toIso8601String());}}
Handling Offline Data with BLoC
Combine local storage with an offline queue to handle mutations when offline. Store pending actions in a local database and sync when connectivity returns.
DARTRead-only
1
classOfflineQueue{
final Box<Map<String, dynamic>> queueBox;OfflineQueue(this.queueBox);voidadd(Map<String, dynamic> operation){
final id = DateTime.now().millisecondsSinceEpoch.toString();
queueBox.put(id, operation);}
List<Map<String, dynamic>>getAll()=> queueBox.values.toList();voidremove(String id)=> queueBox.delete(id);}// In BLoC
on<AddTodo>((event, emit) async {if(awaithasNetwork()){await api.addTodo(event.todo);await localRepository.saveTodo(event.todo);emit(TodoAdded(event.todo));}else{
queue.add({'type':'ADD_TODO','data': event.todo.toJson()});await localRepository.saveTodo(event.todo);// Optimistic updateemit(TodoAdded(event.todo));}});
Best Practices
Use repository pattern – Keep data source logic separate from BLoC.
Initialize storage early – Open databases and boxes before runApp.
Handle errors gracefully – Use try/catch and emit error states.
Implement caching policies – Use TTL, versioning, or manual invalidation.
Test storage interactions – Mock databases or use in‑memory implementations for unit tests.
Consider encryption – For sensitive data, use encrypted boxes (Hive) or encrypted SharedPreferences.
Manage database migrations – For SQLite, version the database and handle schema changes.
Common Mistakes
❌ Blocking the UI thread – Using synchronous storage operations in the main thread.
✅ Use async methods and await properly.
❌ Not closing databases – Can cause resource leaks.
✅ Close boxes/connections when the app terminates or when no longer needed.
❌ Storing too much data in SharedPreferences – Causes performance issues.
✅ Use Hive or SQLite for large datasets.
❌ Forgetting to register adapters – Hive adapters must be registered before opening boxes.
✅ Register adapters in main.
❌ Ignoring versioning – Changing data models without migrations breaks existing storage.
✅ Plan migrations or use serialization that tolerates changes.
Conclusion
Integrating local storage with BLoC gives you the best of both worlds: reactive state management and persistent data. By using repositories to abstract storage, you can switch between Hive, SharedPreferences, or SQLite without affecting your BLoCs. Combine caching strategies and offline queues to build robust, offline‑first Flutter applications.