Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
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
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
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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.// 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.// 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
// 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.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.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.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.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.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;
}
}
}
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.// 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');
}
}
void main() {
Bloc.observer = const AppBlocObserver();
runApp(MyApp());
}
๐งฉ Advanced Patterns
Bloc-to-Bloc Communication
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
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
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
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
class ProductState extends Equatable {
final List<Product> products;
const ProductState(this.products);
@override
List<Object?> get props => [products];
}
2. Avoid Rebuilding Entire Tree
// โ 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
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