معماری 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باشد، دیگر بارگذاری انجام نمیشود.
✅ مزایای این ساختار
- تفکیک مسئولیتها: هر بخش وظیفه خاصی دارد
- قابل تست و توسعه: راحت میتوان تست نوشت یا قابلیت جدید اضافه کرد
- خوانایی بالا: توسعهدهندگان دیگر بهراحتی میتوانند پروژه را دنبال کنند