معماری BLoC (مخفف Business Logic Component) یکی از محبوبترین معماریها در فلاتر برای مدیریت وضعیت (state management) است. این معماری به تفکیک منطق کسبوکار از رابط کاربری کمک میکند و باعث میشود کدها قابل تست، قابل نگهداری و قابل توسعه باشند.

🧠 اجزای اصلی معماری BLoC
1. Event (رویدادها)
رویدادهایی هستند که از سمت UI به BLoC ارسال میشوند. مثلاً:
- کاربر روی دکمه کلیک میکند
- اسکرول به انتهای لیست میرسد
- فرم ارسال میشود
class FetchProducts extends ProductEvent {}
2. State (وضعیتها)
نمایانگر وضعیت فعلی اپلیکیشن هستند. UI بر اساس State تغییر میکند.
class ProductLoading extends ProductState {} class ProductLoaded extends ProductState { final List<Product> products; }
3. Bloc (منطق کسبوکار)
واسط بین Event و State است. وقتی Event دریافت میشود، BLoC آن را پردازش کرده و یک State جدید تولید میکند.
on<FetchProducts>((event, emit) async { emit(ProductLoading()); final products = await repository.fetchProducts(); emit(ProductLoaded(products)); });
4. UI (رابط کاربری)
با استفاده از BlocBuilder
یا BlocListener
به تغییرات State واکنش نشان میدهد.
BlocBuilder<ProductBloc, ProductState>( builder: (context, state) { if (state is ProductLoading) return CircularProgressIndicator(); if (state is ProductLoaded) return ListView(...); return Text('خطا'); }, )
🎯 مزایای معماری BLoC
- جداسازی کامل UI از منطق
- مناسب برای پروژههای بزرگ
- قابل تست بودن منطق
- استفاده از Stream برای مدیریت دادهها
در ادامه یک مثال عملی را با هم بررسی می کنیم:
📁 ساختار پوشهها
lib/ ├── blocs/ │ └── product/ │ ├── product_bloc.dart │ ├── product_event.dart │ └── product_state.dart │ ├── models/ │ └── product.dart │ ├── repositories/ │ └── product_repository.dart │ ├── screens/ │ └── product_list_screen.dart │ ├── widgets/ │ └── product_list_item.dart │ ├── main.dart
📦 پکیجهای مورد نیاز برای مثال
1. flutter_bloc
برای استفاده از کلاسهای Bloc
, BlocBuilder
, BlocProvider
, BlocListener
و مدیریت وضعیتها.
dependencies: flutter_bloc: ^8.1.3
2. equatable
برای مقایسه راحتتر بین Event
و State
بدون نیاز به نوشتن دستی ==
و hashCode
.
dependencies: equatable: ^2.0.5
3. (اختیاری برای تست) bloc_test
و mocktail
اگر بخوای تستهایی مثل blocTest
یا تست ویجت بنویسی، اینها لازم میشن:
dev_dependencies: bloc_test: ^9.1.0 mocktail: ^1.0.0
🧱 ساختار کلی BLoC
1. ProductEvent
abstract class ProductEvent {} class FetchProducts extends ProductEvent { final int page; FetchProducts(this.page); } class AddToFavorites extends ProductEvent { final Product product; AddToFavorites(this.product); }
2. ProductState
abstract class ProductState {} class ProductInitial extends ProductState {} class ProductLoading extends ProductState {} class ProductLoaded extends ProductState { final List<Product> products; final bool hasReachedMax; ProductLoaded({required this.products, required this.hasReachedMax}); } class ProductError extends ProductState { final String message; ProductError(this.message); }
3. ProductBloc
class ProductBloc extends Bloc<ProductEvent, ProductState> { final ProductRepository repository; int currentPage = 1; bool isFetching = false; ProductBloc(this.repository) : super(ProductInitial()) { on<FetchProducts>(_onFetchProducts); on<AddToFavorites>(_onAddToFavorites); } Future<void> _onFetchProducts(FetchProducts event, Emitter<ProductState> emit) async { if (isFetching) return; isFetching = true; try { final currentState = state; List<Product> oldProducts = []; if (currentState is ProductLoaded) { oldProducts = currentState.products; } final newProducts = await repository.fetchProducts(event.page); final hasReachedMax = newProducts.isEmpty; emit(ProductLoaded( products: oldProducts + newProducts, hasReachedMax: hasReachedMax, )); currentPage++; } catch (e) { emit(ProductError(e.toString())); } isFetching = false; } void _onAddToFavorites(AddToFavorites event, Emitter<ProductState> emit) { // اینجا میتونی محصول رو به لیست علاقهمندیها اضافه کنی یا API بزنی // مثلا: repository.addToFavorites(event.product); } }
4.Repository
class ProductRepository { Future<List<Product>> fetchProducts(int page) async { // API call برای دریافت محصولات } Future<void> addToFavorites(Product product) async { // API call برای افزودن به علاقهمندیها } }
🎯 پیادهسازی ویجت لیست محصولات
class ProductListWidget extends StatefulWidget { const ProductListWidget({Key? key}) : super(key: key); @override State<ProductListWidget> createState() => _ProductListWidgetState(); } class _ProductListWidgetState extends State<ProductListWidget> { final ScrollController _scrollController = ScrollController(); late ProductBloc _productBloc; @override void initState() { super.initState(); _productBloc = context.read<ProductBloc>(); _productBloc.add(FetchProducts(1)); // بارگذاری اولیه _scrollController.addListener(() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { final state = _productBloc.state; if (state is ProductLoaded && !state.hasReachedMax) { _productBloc.add(FetchProducts(_productBloc.currentPage)); } } }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder<ProductBloc, ProductState>( builder: (context, state) { if (state is ProductLoading && _productBloc.currentPage == 1) { return const Center(child: CircularProgressIndicator()); } else if (state is ProductLoaded) { return ListView.builder( controller: _scrollController, itemCount: state.products.length + 1, itemBuilder: (context, index) { if (index < state.products.length) { final product = state.products[index]; return ListTile( leading: Image.network(product.imageUrl), title: Text(product.name), subtitle: Text('${product.price} تومان'), trailing: IconButton( icon: const Icon(Icons.favorite_border), onPressed: () { _productBloc.add(AddToFavorites(product)); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${product.name} به علاقهمندیها اضافه شد')), ); }, ), ); } else { return state.hasReachedMax ? const SizedBox.shrink() : const Padding( padding: EdgeInsets.all(16.0), child: Center(child: CircularProgressIndicator()), ); } }, ); } else if (state is ProductError) { return Center(child: Text('خطا: ${state.message}')); } else { return const SizedBox.shrink(); } }, ); } }
✅ نکات مهم
- از
ScrollController
برای تشخیص رسیدن به انتهای لیست استفاده شده. - در
BlocBuilder
وضعیتها بررسی میشوند: بارگذاری، موفق، خطا. - دکمه علاقهمندی با dispatch کردن
AddToFavorites
کار میکند. - اگر
hasReachedMax == true
باشد، دیگر بارگذاری انجام نمیشود.
✅ مزایای این ساختار
- تفکیک مسئولیتها: هر بخش وظیفه خاصی دارد
- قابل تست و توسعه: راحت میتوان تست نوشت یا قابلیت جدید اضافه کرد
- خوانایی بالا: توسعهدهندگان دیگر بهراحتی میتوانند پروژه را دنبال کنند