Tampilan
Bab 25 — HydratedBloc
Naikkan penghitungmu sampai lima, lalu tutup aplikasi dan buka lagi. Angkanya kembali ke nol.
Ini perilaku bawaan setiap aplikasi Flutter: state hidup di memori, dan memori hilang ketika proses dimatikan. Bab ini membahas cara membuatnya bertahan — dengan satu paket yang mengurus hampir seluruhnya secara otomatis.
Masalahnya
bloc membantumu membuat state. Tetapi menyimpannya adalah tugasmu sendiri.
Tanpa bantuan, kamu harus menulis ini di setiap Cubit:
dart
// Pendekatan manual — melelahkan dan mudah salah
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0) {
_muat();
}
Future<void> _muat() async {
final prefs = await SharedPreferences.getInstance();
emit(prefs.getInt('counter') ?? 0);
}
Future<void> increment() async {
final baru = state + 1;
emit(baru);
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('counter', baru); // ingat menyimpan setiap kali
}
}Untuk state berupa int ini masih tertangani. Untuk state berupa objek dengan sepuluh properti, kamu akan menulis serialisasi manual — dan lupa menyimpan di salah satu metode adalah bug yang hampir pasti terjadi.
hydrated_bloc menyelesaikan ini: ia otomatis menyimpan setiap state yang dikeluarkan, dan otomatis memulihkannya saat aplikasi dibuka.
Memasang
yaml
# pubspec.yaml
dependencies:
hydrated_bloc: ^9.1.5
path_provider: ^2.1.5Menyiapkan penyimpanan
Ini langkah sekali di main():
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
// Wajib sebelum memakai plugin apa pun di main()
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(
(await getApplicationDocumentsDirectory()).path,
),
);
runApp(const AplikasiSaya());
}Untuk web
Di web tidak ada direktori dokumen. Pakai direktori khusus web:
dart
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory(
(await getApplicationDocumentsDirectory()).path,
),
);Perhatikan bahwa main() sekarang async. Kalau kamu lupa await, aplikasi akan berjalan sebelum penyimpanan siap, dan Cubit pertama yang dibuat akan melempar error.
HydratedCubit dengan state sederhana
Ubah extends Cubit<T> menjadi extends HydratedCubit<T>, lalu implementasikan dua metode.
dart
// lib/logic/cubit/counter_cubit.dart
import 'package:hydrated_bloc/hydrated_bloc.dart';
class CounterCubit extends HydratedCubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
/// Dipanggil saat aplikasi dibuka.
/// Mengubah data tersimpan kembali menjadi state.
@override
int? fromJson(Map<String, dynamic> json) {
return json['nilai'] as int?;
}
/// Dipanggil setiap kali emit dijalankan.
/// Mengubah state menjadi data yang bisa disimpan.
@override
Map<String, dynamic>? toJson(int state) {
return {'nilai': state};
}
}Itu saja. Tidak ada pemanggilan simpan atau muat di mana pun — HydratedCubit memanggil toJson setiap kali emit berjalan, dan memanggil fromJson saat Cubit dibuat.
APLIKASI DIBUKA SETIAP emit()
─────────────── ─────────────
Cubit dibuat emit(nilaiBaru)
│ │
▼ ▼
fromJson(dataTersimpan) toJson(state)
│ │
▼ ▼
state awal dipulihkan disimpan ke diskNilai kembalian null
Baik fromJson maupun toJson boleh mengembalikan null, dan artinya berbeda:
fromJsonmengembalikannull→ pakai state awal darisuper(...). Ini yang terjadi saat aplikasi pertama kali dipasang.toJsonmengembalikannull→ jangan simpan state ini. Berguna untuk keadaan yang tidak layak dipulihkan, sepertiLoadingatauError.
State kustom
Untuk state berupa objek, toJson dan fromJson harus mengurusnya sendiri.
dart
// lib/logic/cubit/settings_state.dart
import 'package:equatable/equatable.dart';
class SettingsState extends Equatable {
const SettingsState({
this.notifikasiAplikasi = false,
this.notifikasiEmail = false,
this.modeGelap = false,
this.ukuranFont = 14.0,
this.bahasa = 'id',
});
final bool notifikasiAplikasi;
final bool notifikasiEmail;
final bool modeGelap;
final double ukuranFont;
final String bahasa;
SettingsState copyWith({
bool? notifikasiAplikasi,
bool? notifikasiEmail,
bool? modeGelap,
double? ukuranFont,
String? bahasa,
}) {
return SettingsState(
notifikasiAplikasi: notifikasiAplikasi ?? this.notifikasiAplikasi,
notifikasiEmail: notifikasiEmail ?? this.notifikasiEmail,
modeGelap: modeGelap ?? this.modeGelap,
ukuranFont: ukuranFont ?? this.ukuranFont,
bahasa: bahasa ?? this.bahasa,
);
}
Map<String, dynamic> toMap() => {
'notifikasiAplikasi': notifikasiAplikasi,
'notifikasiEmail': notifikasiEmail,
'modeGelap': modeGelap,
'ukuranFont': ukuranFont,
'bahasa': bahasa,
};
factory SettingsState.dariMap(Map<String, dynamic> map) {
return SettingsState(
notifikasiAplikasi: map['notifikasiAplikasi'] as bool? ?? false,
notifikasiEmail: map['notifikasiEmail'] as bool? ?? false,
modeGelap: map['modeGelap'] as bool? ?? false,
ukuranFont: (map['ukuranFont'] as num?)?.toDouble() ?? 14.0,
bahasa: map['bahasa'] as String? ?? 'id',
);
}
@override
List<Object?> get props => [
notifikasiAplikasi,
notifikasiEmail,
modeGelap,
ukuranFont,
bahasa,
];
}Perhatikan bahwa setiap pembacaan di dariMap punya nilai cadangan (?? false, ?? 14.0). Ini bukan kerapian — ini perlindungan terhadap data lama yang tidak punya kunci itu, yang akan kita bahas di bagian migrasi.
Perhatikan juga (map['ukuranFont'] as num?)?.toDouble(). Data JSON bisa menyimpan 14 (int) untuk nilai yang aslinya 14.0 (double), dan melakukan cast langsung ke double akan gagal. Jebakan yang sama dengan yang dibahas di Bab 2.
Cubit-nya:
dart
// lib/logic/cubit/settings_cubit.dart
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'settings_state.dart';
class SettingsCubit extends HydratedCubit<SettingsState> {
SettingsCubit() : super(const SettingsState());
void setNotifikasiAplikasi(bool aktif) {
emit(state.copyWith(notifikasiAplikasi: aktif));
}
void setNotifikasiEmail(bool aktif) {
emit(state.copyWith(notifikasiEmail: aktif));
}
void setModeGelap(bool aktif) {
emit(state.copyWith(modeGelap: aktif));
}
void setUkuranFont(double ukuran) {
emit(state.copyWith(ukuranFont: ukuran));
}
void setBahasa(String kode) {
emit(state.copyWith(bahasa: kode));
}
void resetKeBawaan() => emit(const SettingsState());
@override
SettingsState? fromJson(Map<String, dynamic> json) {
try {
return SettingsState.dariMap(json);
} catch (_) {
// Data rusak atau format lama — kembali ke bawaan
return null;
}
}
@override
Map<String, dynamic>? toJson(SettingsState state) => state.toMap();
}try/catch di fromJson itu penting. Kalau data tersimpan rusak — misalnya karena pembaruan aplikasi mengubah strukturnya — mengembalikan null membuat aplikasi jatuh ke state bawaan alih-alih crash saat dibuka.
Menyimpan daftar objek
Untuk state berupa List<T>, kamu perlu mengurai setiap elemennya.
dart
// lib/logic/cubit/todo_cubit.dart
import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../data/models/todo.dart';
class TodoCubit extends HydratedCubit<List<Todo>> {
TodoCubit() : super(const []);
void tambah(String judul) {
emit([...state, Todo(judul: judul)]);
}
void toggleSelesai(String id) {
emit([
for (final t in state)
if (t.id == id) t.copyWith(selesai: !t.selesai) else t,
]);
}
void hapus(String id) {
emit(state.where((t) => t.id != id).toList());
}
void hapusYangSelesai() {
emit(state.where((t) => !t.selesai).toList());
}
@override
List<Todo>? fromJson(Map<String, dynamic> json) {
try {
final daftar = json['todos'] as List<dynamic>?;
if (daftar == null) return null;
return daftar
.map((e) => Todo.dariMap(e as Map<String, dynamic>))
.toList();
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(List<Todo> state) {
return {'todos': state.map((t) => t.toMap()).toList()};
}
}toJson harus mengembalikan Map
Perhatikan bahwa toJson selalu mengembalikan Map<String, dynamic>, bukan List. Kalau state-mu berupa list, bungkus di dalam sebuah map dengan kunci — seperti {'todos': [...]} di atas. Ini persyaratan API-nya.
Modelnya:
dart
// lib/data/models/todo.dart
import 'package:equatable/equatable.dart';
import 'package:uuid/uuid.dart';
const _uuid = Uuid();
class Todo extends Equatable {
Todo({
String? id,
required this.judul,
this.selesai = false,
DateTime? dibuatPada,
}) : id = id ?? _uuid.v4(),
dibuatPada = dibuatPada ?? DateTime.now();
final String id;
final String judul;
final bool selesai;
final DateTime dibuatPada;
Todo copyWith({String? judul, bool? selesai}) {
return Todo(
id: id,
judul: judul ?? this.judul,
selesai: selesai ?? this.selesai,
dibuatPada: dibuatPada,
);
}
Map<String, dynamic> toMap() => {
'id': id,
'judul': judul,
'selesai': selesai,
// DateTime tidak bisa langsung masuk JSON
'dibuatPada': dibuatPada.toIso8601String(),
};
factory Todo.dariMap(Map<String, dynamic> map) {
return Todo(
id: map['id'] as String,
judul: map['judul'] as String,
selesai: map['selesai'] as bool? ?? false,
dibuatPada: DateTime.parse(map['dibuatPada'] as String),
);
}
@override
List<Object?> get props => [id, judul, selesai, dibuatPada];
}Perhatikan konversi DateTime. JSON hanya mengenal string, angka, boolean, list, dan map — jadi DateTime harus diubah menjadi salah satunya. toIso8601String() menghasilkan teks yang bisa dibaca manusia dan diurutkan dengan benar.
HydratedBloc dengan event
Untuk Bloc (bukan Cubit), polanya identik — hanya nama kelas dasarnya yang berbeda.
dart
class KeranjangBloc extends HydratedBloc<KeranjangEvent, KeranjangState> {
KeranjangBloc() : super(const KeranjangState()) {
on<ItemDitambahkan>((event, emit) {
emit(state.copyWith(items: [...state.items, event.item]));
});
on<ItemDihapus>((event, emit) {
emit(state.copyWith(
items: state.items.where((i) => i.id != event.id).toList(),
));
});
on<KeranjangDikosongkan>((event, emit) {
emit(const KeranjangState());
});
}
@override
KeranjangState? fromJson(Map<String, dynamic> json) {
try {
return KeranjangState.dariMap(json);
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(KeranjangState state) => state.toMap();
}Menyimpan sebagian state saja
Sering kali tidak semua bagian state layak disimpan. Status pemuatan dan pesan kesalahan, misalnya, tidak masuk akal untuk dipulihkan.
dart
class ProdukCubit extends HydratedCubit<ProdukState> {
ProdukCubit(this._repo) : super(const ProdukState());
final ProdukRepository _repo;
Future<void> muat() async {
emit(state.copyWith(sedangMemuat: true, pesanKesalahan: null));
try {
final daftar = await _repo.ambilSemua();
emit(state.copyWith(daftar: daftar, sedangMemuat: false));
} catch (_) {
emit(state.copyWith(
sedangMemuat: false,
pesanKesalahan: 'Gagal memuat produk.',
));
}
}
@override
ProdukState? fromJson(Map<String, dynamic> json) {
try {
final daftar = (json['daftar'] as List<dynamic>)
.map((e) => Produk.dariMap(e as Map<String, dynamic>))
.toList();
// Pulihkan hanya datanya. Status pemuatan selalu dimulai bersih.
return ProdukState(daftar: daftar);
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(ProdukState state) {
// Jangan simpan saat sedang memuat atau saat gagal
if (state.sedangMemuat || state.pesanKesalahan != null) return null;
return {'daftar': state.daftar.map((p) => p.toMap()).toList()};
}
}Ini pola yang sangat berguna: aplikasi dibuka dan langsung menampilkan data terakhir yang berhasil dimuat, sambil menyegarkannya di latar belakang. Pengguna tidak pernah melihat layar kosong.
Migrasi ketika struktur berubah
Ini masalah nyata yang muncul di aplikasi produksi. Pengguna memasang versi 1, lalu memperbarui ke versi 2 yang struktur state-nya berbeda. Data lama tidak cocok lagi.
Cara paling sederhana adalah memberi nomor versi:
dart
class SettingsCubit extends HydratedCubit<SettingsState> {
SettingsCubit() : super(const SettingsState());
static const _versiSkema = 2;
@override
SettingsState? fromJson(Map<String, dynamic> json) {
try {
final versi = json['_versi'] as int? ?? 1;
return switch (versi) {
1 => _dariVersi1(json),
2 => SettingsState.dariMap(json),
_ => null, // versi tidak dikenal, pakai bawaan
};
} catch (_) {
return null;
}
}
/// Versi 1 memakai 'darkMode', versi 2 memakai 'modeGelap'.
SettingsState _dariVersi1(Map<String, dynamic> json) {
return SettingsState(
modeGelap: json['darkMode'] as bool? ?? false,
notifikasiAplikasi: json['appNotifications'] as bool? ?? false,
// Properti baru di versi 2 memakai nilai bawaan
);
}
@override
Map<String, dynamic>? toJson(SettingsState state) => {
...state.toMap(),
'_versi': _versiSkema,
};
}Alternatif yang lebih kasar tetapi kadang tepat: buang saja data lama.
dart
@override
SettingsState? fromJson(Map<String, dynamic> json) {
if ((json['_versi'] as int? ?? 0) != _versiSkema) {
return null; // pengguna mulai dari bawaan
}
return SettingsState.dariMap(json);
}Untuk pengaturan, ini mungkin tidak apa-apa. Untuk daftar tugas yang dibuat pengguna, tentu tidak.
Mengelola penyimpanan
dart
// Menghapus data satu Cubit
await context.read<CounterCubit>().clear();
// Menghapus SEMUA data hydrated (misalnya saat keluar akun)
await HydratedBloc.storage.clear();
// Membaca langsung (jarang diperlukan)
final data = HydratedBloc.storage.read('CounterCubit');Pola keluar akun yang aman:
dart
Future<void> keluar() async {
await _authRepo.keluar();
// Bersihkan seluruh state tersimpan agar pengguna berikutnya
// tidak melihat data pengguna sebelumnya
await HydratedBloc.storage.clear();
emit(const AuthLoggedOut());
}Jangan simpan data sensitif
hydrated_bloc menyimpan data sebagai berkas biasa di direktori aplikasi — tidak terenkripsi. Perlakuannya sama dengan SharedPreferences di Bab 15.
Jangan simpan token autentikasi, kata sandi, data pembayaran, atau informasi pribadi sensitif di sini. Untuk itu pakai flutter_secure_storage, dan buat toJson mengembalikan null untuk bagian-bagian sensitif:
dart
@override
Map<String, dynamic>? toJson(AuthState state) {
// Simpan hanya bahwa pengguna pernah masuk, bukan tokennya
if (state is AuthSuccess) {
return {'idPengguna': state.pengguna.id};
}
return null;
}Menguji HydratedBloc
Pengujian butuh penyimpanan palsu, karena tidak ada direktori dokumen di lingkungan uji.
dart
// test/settings_cubit_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:mocktail/mocktail.dart';
class MockStorage extends Mock implements Storage {}
void main() {
late Storage storage;
setUp(() {
storage = MockStorage();
when(() => storage.write(any(), any<dynamic>()))
.thenAnswer((_) async {});
HydratedBloc.storage = storage;
});
test('state awal adalah bawaan ketika penyimpanan kosong', () {
when(() => storage.read(any())).thenReturn(null);
final cubit = SettingsCubit();
expect(cubit.state, const SettingsState());
});
test('memulihkan state dari penyimpanan', () {
when(() => storage.read(any())).thenReturn({
'modeGelap': true,
'ukuranFont': 18.0,
'bahasa': 'en',
});
final cubit = SettingsCubit();
expect(cubit.state.modeGelap, isTrue);
expect(cubit.state.ukuranFont, 18.0);
});
test('mengembalikan bawaan ketika data rusak', () {
when(() => storage.read(any())).thenReturn({'modeGelap': 'bukan bool'});
final cubit = SettingsCubit();
expect(cubit.state, const SettingsState());
});
}Pengujian ketiga itu penting: ia membuktikan bahwa try/catch di fromJson benar-benar melindungi aplikasi dari data rusak. Pengujian BLoC dibahas lebih lengkap di Bab 26.
Kapan memakai, kapan tidak
Cocok untuk:
- Preferensi pengguna — tema, bahasa, ukuran font.
- Keranjang belanja yang belum di-checkout.
- Draf yang belum dikirim.
- Cache data agar aplikasi tidak menampilkan layar kosong saat dibuka.
- Progres onboarding.
Tidak cocok untuk:
- Data sensitif — token, kata sandi, informasi pembayaran.
- Data besar. Setiap
emitmenulis ke disk, jadi state berukuran megabita akan memperlambat aplikasi. - Data yang harus sinkron antar perangkat — itu tugas backend.
- State sementara seperti
LoadingdanError.
Alternatif
Kalau kamu butuh kendali lebih halus atas kapan menyimpan, atau state-mu besar, lebih baik memakai sqflite atau Hive secara eksplisit di lapisan repository — seperti pola di Bab 22. hydrated_bloc unggul justru karena kesederhanaannya, dan itu berhenti menjadi keunggulan ketika kebutuhanmu rumit.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Konversi dan Rusakkan — ⭐⭐ · 40–60 menit
Tantangan: Ubah salah satu Cubit yang sudah pernah kamu buat menjadi HydratedCubit, dengan state kustom yang berisi setidaknya satu DateTime, satu double, dan satu daftar objek. Lalu rusak datanya dengan sengaja — ubah isi penyimpanan menjadi JSON yang tidak sesuai — dan pastikan aplikasi tetap terbuka.
Kriteria selesai:
toJsonmengembalikanMap, dengan daftar dibungkus di dalamnya memakai kunci — bukanListdi tingkat teratas.DateTimedikonversi dengantoIso8601String()saat menyimpan danDateTime.parse()saat memulihkan, dan angka pecahan melewatias num?sebelum.toDouble().- Setiap pembacaan di
fromJsonpunya nilai cadangan, dan keseluruhannya dibungkustry/catchsehingga data rusak menghasilkan state awal, bukan crash. - Status memuat dan gagal tidak disimpan —
toJsonmengembalikannulluntuk keadaan itu.
Petunjuk: Kriteria terakhir adalah yang paling terasa hasilnya. Kalau status "sedang memuat" ikut tersimpan, aplikasi yang dibuka kembali akan memulai hidupnya dalam keadaan memuat sesuatu yang tidak sedang dimuat — pengguna melihat pemutar berputar selamanya. Mengembalikan null dari toJson untuk keadaan itu berarti state terakhir yang berguna yang tersimpan, sehingga aplikasi langsung menampilkan data terakhir tanpa layar kosong sama sekali. Untuk merusak data dengan sengaja, cara termudah adalah mengubah bentuk state di kode (misalnya mengganti nama sebuah field) lalu menjalankan aplikasi tanpa menghapusnya dari perangkat — persis seperti yang dialami pengguna saat kamu merilis pembaruan. Itu juga alasan nomor versi skema ada.
Variasi 2: Pemutar Podcast yang Selektif — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun state pemutar podcast yang berisi campuran hal yang layak disimpan dan yang tidak: episode terakhir dan posisi dengar layak; daftar hasil pencarian, status buffering, dan pesan kesalahan tidak. Simpan hanya sebagiannya.
Kriteria selesai:
toJsonhanya menuliskan field yang memang layak bertahan.fromJsonmengembalikan state yang field sementaranya diisi nilai awal yang benar, bukan nilai lama.- Membuka aplikasi kembali menampilkan episode terakhir seketika, tanpa indikator memuat.
- Kamu bisa menyebut satu field yang keputusannya tidak jelas, dan alasan pilihanmu.
Petunjuk: Aturan praktis untuk memilah: simpan yang mewakili niat atau kemajuan pengguna, buang yang mewakili keadaan sesaat mesin. Posisi dengar adalah kemajuan pengguna; status buffering adalah keadaan mesin. Field yang keputusannya tidak jelas biasanya berada di perbatasan — kata kunci pencarian terakhir, misalnya, bisa dianggap kenyamanan atau bisa dianggap sampah, dan jawabannya bergantung pada aplikasinya.
Variasi 3: Migrasi Skema Tersimpan — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Buat HydratedCubit versi 1, isi datanya, lalu kembangkan state-nya sampai versi 3 lewat dua perubahan yang merusak bentuk: mengganti nama sebuah field, dan mengubah satu field tunggal menjadi daftar. Pengguna dari versi berapa pun harus naik ke versi 3 tanpa kehilangan data.
Kriteria selesai:
- Ada nomor versi skema yang ikut tersimpan di dalam JSON.
fromJsonmenangani lompatan dari versi 1 langsung ke 3.- Data dari versi lama benar-benar terbawa, bukan diganti nilai awal.
- Kamu sudah menguji ketiga jalur dengan menjalankan aplikasi tanpa menghapusnya dari perangkat.
Petunjuk: Tanpa nomor versi, satu-satunya cara fromJson menebak bentuk data adalah dengan memeriksa keberadaan field — cara yang bekerja untuk satu perubahan dan menjadi kacau setelah tiga. Menuliskan versi sejak awal terasa berlebihan sampai perubahan pertama datang. Pola migrasinya sama dengan onUpgrade pada SQLite di Bab 15: rangkaian transformasi bertahap, bukan satu percabangan besar per versi.
Ikhtisar
HydratedBlocdanHydratedCubitotomatis menyimpan setiap state yang di-emitdan otomatis memulihkannya saat aplikasi dibuka.- Siapkan
HydratedBloc.storagedimain()yangasync, setelahWidgetsFlutterBinding.ensureInitialized(). - Implementasikan dua metode:
fromJson(pulihkan) dantoJson(simpan). fromJsonmengembalikannull→ pakai state awal.toJsonmengembalikannull→ jangan simpan state ini.toJsonharus mengembalikanMap, bukanList. Bungkus list di dalam map dengan kunci.- Beri nilai cadangan pada setiap pembacaan di
dariMap(?? false), dan bungkusfromJsondengantry/catchagar data rusak tidak membuat aplikasi crash. DateTimeharus dikonversi —toIso8601String()saat menyimpan,DateTime.parse()saat memulihkan.- Lewati
as num?sebelum.toDouble()karena JSON bisa menyimpan14untuk nilai14.0. - Jangan simpan status
LoadingdanError. KembalikannulldaritoJsonuntuk keadaan itu — hasilnya aplikasi langsung menampilkan data terakhir tanpa layar kosong. - Tambahkan nomor versi skema agar pembaruan aplikasi tidak merusak data pengguna lama.
- Jangan simpan data sensitif — penyimpanannya tidak terenkripsi. Pakai
flutter_secure_storageuntuk token dan kata sandi. - Bersihkan dengan
HydratedBloc.storage.clear()saat pengguna keluar akun.
Berikutnya: Bab 26 — Testing & Debugging BLoC, penutup Bagian III.
Transkrip asli
Disintesis dari 2_flutter_bloc-zero-to-hero/10_local-storage-with-hydrated-bloc.md (mempertahankan state counter setelah aplikasi ditutup, fromJson/toJson). Lihat PDF BLoC Zero to Hero.