State Management dengan BLoC
MStore menggunakan BLoC (Business Logic Component) Pattern untuk state management yang scalable dan testable.π― Mengapa BLoC?
Keuntungan BLoC Pattern
- Separation of Concerns - Business logic terpisah dari UI
- Testability - Mudah di-unit test tanpa UI
- Reusability - BLoC bisa digunakan di multiple screens
- Predictability - State changes melalui events yang jelas
- Debugging - Easy to track state changes
- Platform Agnostic - Bisa digunakan di Flutter, AngularDart, dll
π¦ Dependencies
Copy
dependencies:
bloc: ^9.0.0
flutter_bloc: ^9.1.1
hydrated_bloc: ^10.1.1 # Untuk persist state
equatable: ^2.0.7 # Untuk value equality
ποΈ Arsitektur BLoC
Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UI Layer β
β (Widgets, Pages) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
BlocBuilder/
BlocListener/
BlocConsumer
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BLoC Layer β
β (Business Logic) β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Event β β β BLoC β β β State β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Repository Layer β
β (Data Access) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Struktur BLoC
1. Events
Events adalah input ke BLoC yang memicu state changes.Copy
// features/product/bloc/product_event.dart
abstract class ProductEvent extends Equatable {
const ProductEvent();
@override
List<Object?> get props => [];
}
class LoadProducts extends ProductEvent {
const LoadProducts();
}
class LoadProductById extends ProductEvent {
final String productId;
const LoadProductById(this.productId);
@override
List<Object?> get props => [productId];
}
class CreateProduct extends ProductEvent {
final Product product;
const CreateProduct(this.product);
@override
List<Object?> get props => [product];
}
class UpdateProduct extends ProductEvent {
final String productId;
final Product product;
const UpdateProduct({
required this.productId,
required this.product,
});
@override
List<Object?> get props => [productId, product];
}
class DeleteProduct extends ProductEvent {
final String productId;
const DeleteProduct(this.productId);
@override
List<Object?> get props => [productId];
}
class SearchProducts extends ProductEvent {
final String query;
const SearchProducts(this.query);
@override
List<Object?> get props => [query];
}
2. States
States merepresentasikan kondisi UI pada waktu tertentu.Copy
// features/product/bloc/product_state.dart
abstract class ProductState extends Equatable {
const ProductState();
@override
List<Object?> get props => [];
}
class ProductInitial extends ProductState {
const ProductInitial();
}
class ProductLoading extends ProductState {
const ProductLoading();
}
class ProductLoaded extends ProductState {
final List<Product> products;
const ProductLoaded(this.products);
@override
List<Object?> get props => [products];
}
class ProductDetailLoaded extends ProductState {
final Product product;
const ProductDetailLoaded(this.product);
@override
List<Object?> get props => [product];
}
class ProductError extends ProductState {
final String message;
const ProductError(this.message);
@override
List<Object?> get props => [message];
}
class ProductOperationSuccess extends ProductState {
final String message;
const ProductOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
3. BLoC Implementation
Copy
// features/product/bloc/product_bloc.dart
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository _productRepository;
ProductBloc({
required ProductRepository productRepository,
}) : _productRepository = productRepository,
super(const ProductInitial()) {
// Register event handlers
on<LoadProducts>(_onLoadProducts);
on<LoadProductById>(_onLoadProductById);
on<CreateProduct>(_onCreateProduct);
on<UpdateProduct>(_onUpdateProduct);
on<DeleteProduct>(_onDeleteProduct);
on<SearchProducts>(_onSearchProducts);
}
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductState> emit,
) async {
emit(const ProductLoading());
final result = await _productRepository.getProducts();
result.fold(
(failure) => emit(ProductError(failure.message)),
(products) => emit(ProductLoaded(products)),
);
}
Future<void> _onLoadProductById(
LoadProductById event,
Emitter<ProductState> emit,
) async {
emit(const ProductLoading());
final result = await _productRepository.getProductById(event.productId);
result.fold(
(failure) => emit(ProductError(failure.message)),
(product) => emit(ProductDetailLoaded(product)),
);
}
Future<void> _onCreateProduct(
CreateProduct event,
Emitter<ProductState> emit,
) async {
emit(const ProductLoading());
final result = await _productRepository.createProduct(event.product);
result.fold(
(failure) => emit(ProductError(failure.message)),
(_) {
emit(const ProductOperationSuccess('Product created successfully'));
// Reload products
add(const LoadProducts());
},
);
}
Future<void> _onUpdateProduct(
UpdateProduct event,
Emitter<ProductState> emit,
) async {
emit(const ProductLoading());
final result = await _productRepository.updateProduct(
event.productId,
event.product,
);
result.fold(
(failure) => emit(ProductError(failure.message)),
(_) {
emit(const ProductOperationSuccess('Product updated successfully'));
add(const LoadProducts());
},
);
}
Future<void> _onDeleteProduct(
DeleteProduct event,
Emitter<ProductState> emit,
) async {
emit(const ProductLoading());
final result = await _productRepository.deleteProduct(event.productId);
result.fold(
(failure) => emit(ProductError(failure.message)),
(_) {
emit(const ProductOperationSuccess('Product deleted successfully'));
add(const LoadProducts());
},
);
}
Future<void> _onSearchProducts(
SearchProducts event,
Emitter<ProductState> emit,
) async {
if (event.query.isEmpty) {
add(const LoadProducts());
return;
}
emit(const ProductLoading());
final result = await _productRepository.searchProducts(event.query);
result.fold(
(failure) => emit(ProductError(failure.message)),
(products) => emit(ProductLoaded(products)),
);
}
}
π¨ UI Integration
BlocProvider
Menyediakan BLoC ke widget tree.Copy
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<ProductBloc>()..add(const LoadProducts()),
child: ProductListView(),
);
}
}
BlocBuilder
Rebuild widget saat state berubah.Copy
class ProductListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is ProductError) {
return Center(child: Text(state.message));
}
if (state is ProductLoaded) {
return ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
final product = state.products[index];
return ProductListItem(product: product);
},
);
}
return const SizedBox.shrink();
},
);
}
}
BlocListener
Listen state changes tanpa rebuild.Copy
class ProductFormPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
Navigator.pop(context);
}
if (state is ProductError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
child: ProductForm(),
);
}
}
BlocConsumer
Kombinasi BlocBuilder + BlocListener.Copy
class ProductDetailPage extends StatelessWidget {
final String productId;
const ProductDetailPage({required this.productId});
@override
Widget build(BuildContext context) {
return BlocConsumer<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
builder: (context, state) {
if (state is ProductLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is ProductDetailLoaded) {
return ProductDetailView(product: state.product);
}
return const SizedBox.shrink();
},
);
}
}
πΎ Hydrated BLoC (Persistent State)
Untuk persist state across app restarts.Copy
class SettingsBloc extends HydratedBloc<SettingsEvent, SettingsState> {
SettingsBloc() : super(const SettingsState.initial()) {
on<UpdateTheme>(_onUpdateTheme);
on<UpdateLanguage>(_onUpdateLanguage);
}
@override
SettingsState? fromJson(Map<String, dynamic> json) {
try {
return SettingsState.fromJson(json);
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(SettingsState state) {
try {
return state.toJson();
} catch (_) {
return null;
}
}
}
Copy
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final directory = await getApplicationDocumentsDirectory();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(directory.path),
);
runApp(MyApp());
}
π BLoC Observer
Global observer untuk logging dan debugging.Copy
// lib/core/observers/app_bloc_observer.dart
class AppBlocObserver extends BlocObserver {
const AppBlocObserver();
@override
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
AppLog.i('bloc.${bloc.runtimeType}', 'created');
}
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
AppLog.i('bloc.${bloc.runtimeType}', 'event: $event');
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
AppLog.i('bloc.${bloc.runtimeType}', 'state_change', meta: {
'current': change.currentState.runtimeType.toString(),
'next': change.nextState.runtimeType.toString(),
});
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
AppLog.i('bloc.${bloc.runtimeType}', 'transition', meta: {
'event': transition.event.runtimeType.toString(),
'current': transition.currentState.runtimeType.toString(),
'next': transition.nextState.runtimeType.toString(),
});
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
AppLog.e('bloc.${bloc.runtimeType}', 'error', error, stackTrace);
}
@override
void onClose(BlocBase bloc) {
super.onClose(bloc);
AppLog.i('bloc.${bloc.runtimeType}', 'closed');
}
}
Copy
void main() {
Bloc.observer = const AppBlocObserver();
runApp(MyApp());
}
π§© Advanced Patterns
Bloc-to-Bloc Communication
Copy
class CartBloc extends Bloc<CartEvent, CartState> {
final ProductBloc _productBloc;
StreamSubscription? _productSubscription;
CartBloc({required ProductBloc productBloc})
: _productBloc = productBloc,
super(const CartInitial()) {
// Listen to product changes
_productSubscription = _productBloc.stream.listen((state) {
if (state is ProductLoaded) {
add(UpdateAvailableProducts(state.products));
}
});
}
@override
Future<void> close() {
_productSubscription?.cancel();
return super.close();
}
}
Debouncing Events
Copy
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(const SearchInitial()) {
on<SearchQueryChanged>(
_onSearchQueryChanged,
transformer: debounce(const Duration(milliseconds: 300)),
);
}
Future<void> _onSearchQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,
) async {
// Search logic
}
}
// Transformer
EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
Throttling Events
Copy
on<ButtonPressed>(
_onButtonPressed,
transformer: throttle(const Duration(seconds: 1)),
);
EventTransformer<T> throttle<T>(Duration duration) {
return (events, mapper) => events.throttleTime(duration).flatMap(mapper);
}
π§ͺ Testing BLoC
Unit Test
Copy
void main() {
group('ProductBloc', () {
late ProductBloc bloc;
late MockProductRepository mockRepository;
setUp(() {
mockRepository = MockProductRepository();
bloc = ProductBloc(productRepository: mockRepository);
});
tearDown(() {
bloc.close();
});
test('initial state is ProductInitial', () {
expect(bloc.state, equals(const ProductInitial()));
});
blocTest<ProductBloc, ProductState>(
'emits [ProductLoading, ProductLoaded] when LoadProducts succeeds',
build: () {
when(() => mockRepository.getProducts()).thenAnswer(
(_) async => Right([Product(id: '1', name: 'Test')]),
);
return bloc;
},
act: (bloc) => bloc.add(const LoadProducts()),
expect: () => [
const ProductLoading(),
isA<ProductLoaded>()
.having((s) => s.products.length, 'products length', 1),
],
);
blocTest<ProductBloc, ProductState>(
'emits [ProductLoading, ProductError] when LoadProducts fails',
build: () {
when(() => mockRepository.getProducts()).thenAnswer(
(_) async => Left(ServerFailure('Server error')),
);
return bloc;
},
act: (bloc) => bloc.add(const LoadProducts()),
expect: () => [
const ProductLoading(),
const ProductError('Server error'),
],
);
});
}
π Performance Tips
1. Use Equatable
Copy
class ProductState extends Equatable {
final List<Product> products;
const ProductState(this.products);
@override
List<Object?> get props => [products];
}
2. Avoid Rebuilding Entire Tree
Copy
// β Bad: Rebuilds entire page
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return EntirePage(state: state);
},
)
// β
Good: Only rebuilds specific widget
Column(
children: [
StaticHeader(),
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
return ProductList(products: state.products);
},
),
StaticFooter(),
],
)
3. Use buildWhen
Copy
BlocBuilder<ProductBloc, ProductState>(
buildWhen: (previous, current) {
// Only rebuild when products actually change
return previous.products != current.products;
},
builder: (context, state) {
return ProductList(products: state.products);
},
)
π Best Practices
- Single Responsibility - Satu BLoC untuk satu feature
- Immutable States - Gunakan
@immutableatauconst - Equatable - Implement untuk efficient rebuilds
- Error Handling - Selalu handle error states
- Loading States - Show loading indicators
- Dispose - Close BLoC saat tidak digunakan
- Testing - Write unit tests untuk semua BLoCs
- Logging - Use BlocObserver untuk debugging
Next Steps
- π Dependency Injection
- ποΈ Database & Storage
- π§ͺ Testing Strategy
BLoC Pattern memberikan:
- β Clear separation of concerns
- β Predictable state management
- β Easy testing
- β Scalable architecture