هناك طريقة وهي استخدام StreamController بحيث هي طريقة من طرق إدارة الحالة في فلاتر. و الأمر معقد جداً و يجب أن تكون على دراية جيدة بكيفية استخدام StreamController. لاحظ بأنه يجب أن تقوم بتعلم StreamController حتى تفهم الإجابة جيداً لأنه في الوهلة الأولى ستجد أن الأمر قد يكون فيه صعوبة
يمكنك إنشاء ملف وليكن مسؤول عن التنصت على أي حدث جديد و يعكسه على الواجهة أي يقوم بتنفيذ هذا الحدث على الواجهة و الميزة في ذلك إنها تقوم بتنفيذ الحدث بدون اللجوء إلى بناء نفسها من الصفر و هذا الملف فليكن اسمه StreamProductsBloc.dart ويمكننا تعريف عدة متغيرات في هذا الملف بهذا الشكل
List<ProductsModel>? products;
final StreamController<List<ProductsModel>> _productsController = StreamController<List<ProductsModel>>.broadcast();
final StreamController<int> _categoryController = StreamController<int>.broadcast();
Stream<List<ProductsModel>> get productsStream => _productsController.stream;
StreamSink<int> get fetchProducts => _categoryController.sink;
Stream<int> get getProductsForCategoriescategory => _categoryController.stream;
APIService? apiService;
int categoryID = 0;
List<ProductsModel>? products;
بحيث أول شيء نقوم بتعريف الكلاس مودل الخاص بالمنتجات.
final StreamController<List<ProductsModel>> _productsController = StreamController<List<ProductsModel>>.broadcast();
و من ثم نقوم بتعريف StreamController ليقوم بالتحكم في العملية , ويتم توفير واجهات مختلفة لإنشاء تدفقات أحداث متنوعة.
final StreamController<int> _categoryController = StreamController<int>.broadcast();
وحدة تحكم حيث يمكن الاستماع إليه أكثر من مرة .
Stream<List<ProductsModel>> get productsStream => _productsController.stream;
ثم نقوم بالحصول على البث للمراقبة.
StreamSink<int> get fetchProducts => _categoryController.sink;
ثم نقوم بإدخال حدث إضافي عن طريق StreamSink.
StreamProductsBloc()
{
//this.categoryID = categoryID;
this.products = [];
apiService = APIService();
_productsController.add(products!);
_categoryController.add(this.categoryID);
_categoryController.stream.listen(_fetchCategoriesFromApi);
}
Future<void> _fetchCategoriesFromApi(int category) async {
this.products = await apiService!.getProductsForCategories(category);
_productsController.add(this.products!);
}
@override
void dispose(){
_productsController.close();
_categoryController.close();
}
نقوم بإنشاء دالة جلب بيانات التصنيف عند تمرير id التصنيف و نقوم بإرجاع جميع المنتجات و من ثم نقوم بإضافة هذه البيانات إلى وحدة التحكم.
Future<void> _fetchCategoriesFromApi(int category) async {
this.products = await apiService!.getProductsForCategories(category);
_productsController.add(this.products!);
}
ثم نقوم بإضافة المنتجات و id التصنيف إلى وحدة التحكم في دالة StreamProductsBloc ثم نقوم بمراقبة الحدث عن طريق استخدام listen
StreamProductsBloc()
{
//this.categoryID = categoryID;
this.products = [];
apiService = APIService();
_productsController.add(products!);
_categoryController.add(this.categoryID);
_categoryController.stream.listen(_fetchCategoriesFromApi);
}
ثم نقوم بإنشاء ملف واجهة عرض البيانات وليكن مثلاً باسم products_screen.dart و نقوم أولاً بإنشاء نسخة جديدة من الملف الذي قمنا بإنشائه مسبقاً
StreamProductsBloc streamProductsBloc = StreamProductsBloc();
ثم ننشأ عدة متغيرات و نعرف TabController لوضع التصنيفات في هذه TabController
bool get wantKeepAlive => true;
HelperApi helperApi = new HelperApi();
int currentIndex = 0;
late TabController _tabController;
ثم نقوم بصنع Widget خاصة بعرض التصنيفات في TabController
Widget _categories(List<CategoryModel> categoryModel, BuildContext context) {
_tabController = TabController(
initialIndex: 0,
length: categoryModel.length,
vsync: this,
);
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).products),
bottom: TabBar(
indicatorColor: Color(0XFF117182),
indicatorWeight: 5,
controller: _tabController,
tabs: _tabs(categoryModel),
isScrollable: true,
onTap: (int index) {
streamProductsBloc.fetchProducts
.add(this.productsCategories[index].id!);
},
),
),
body: Container(
child: StreamBuilder(
stream: streamProductsBloc.productsStream,
builder: (context, AsyncSnapshot<List<ProductsModel>>? snapshot) {
switch (snapshot!.connectionState) {
case ConnectionState.none:
// ignore: todo
// TODO: Handle this case.
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.waiting:
// ignore: todo
// TODO: Handle this case.
return ShimmerGrid();
case ConnectionState.done:
case ConnectionState.active:
// ignore: todo
// TODO: Handle this case.
if (snapshot.hasError) {
return Center(
child: ShimmerGrid(),
);
} else {
if (!snapshot.hasData) {
return ShimmerGrid();
} else {
return _drawProducts(snapshot.data!, context);
}
}
}
},
),
),
);
}
لاحظ أننا نقوم بجلب منتجات كل تصنيف عند الضغط عليها عن طريق استخدام id هذا التصنيف
streamProductsBloc.fetchProducts.add(this.productsCategories[index].id!);
و من ثم نقوم بإضافة هذا الحدث إلى streamProductBloc .
ثم نقوم برسم Widget الخاص بالمنتجات
Widget _drawProducts(List<ProductsModel> products, BuildContext context) {
final cart = Provider.of<Cart>(context, listen: false);
var initModel = Provider.of<InitModel>(context, listen: false,);
var auth = Provider.of<AuthProvider>(context, listen: false);
return Consumer<WishListProvider>(
builder: (BuildContext context, wishListData, child) {
return Container(
padding: EdgeInsets.only(top: 24),
child: Column(
children: [
Flexible(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 1 / 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20),
itemCount: products.length,
itemBuilder: (context, index) {
return Card(
clipBehavior: Clip.hardEdge,
elevation: 0.9,
child: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () {
_gotoSingleProduct(products[index], context);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
// ignore: unnecessary_null_comparison
child: Stack(
children: [
(products[index].images![0].src != null)
? Image.network(
products[index].images![0].src!,
width:
MediaQuery
.of(context)
.size
.width,
height: MediaQuery
.of(context)
.size
.height,
fit: BoxFit.cover,
)
: Image.asset(
"assets/images/1.jpeg",
width:
MediaQuery
.of(context)
.size
.width,
height: MediaQuery
.of(context)
.size
.height,
fit: BoxFit.fill,
),
Positioned.directional(
textDirection: Directionality.of(
context),
child: Container(
margin: EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 10.0),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius:
BorderRadius.circular(50.0)),
child: IconButton(
icon: Icon(
Icons.favorite_outline),
onPressed: () {
//wishListData.getCount(products[index].id!, auth.id!);
wishListData.addNewWishList(
price: products[index]
.price!
.toString(),
product: products[index]
.name!,
image: products[index]
.images![0]
.src!,
productId: products[index]
.id,
userId: auth.id);
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
S
.of(context)
.add_product_to_wishlist,
),
duration: Duration(
seconds: 2),
),
);
},
iconSize: 20.0,
),
),
)
],
)
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
products[index].name!.substring(0, 10),
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(initModel.currency! +
products[index].price!,
style: TextStyle(
color: Colors.black,
fontSize: 15.0,
),
),
),
IconButton(
icon: Icon(
Icons.add_shopping_cart_outlined),
onPressed: () {
//if(auth.status == Status.Authenticated){
cart.addItem(
products[index].id.toString(),
10.0,
products[index].name!.toString(),
products[index].images![0].src!,
1,
products[index].id.toString());
ScaffoldMessenger.of(context)
.hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
S
.of(context)
.product_added_to_the_cart,
),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: S
.of(context)
.undo,
onPressed: () {
cart.reomveSingleItem(
products[index]
.id
.toString());
},
),
),
);
},
),
],
),
Text(products[index].stockStatus!,
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 15,
),),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
RatingBar.builder(
initialRating: products[index]
.ratingCount!
.toDouble(),
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemSize: 15.0,
itemCount: 5,
itemPadding:
EdgeInsets.symmetric(horizontal: 4.0),
itemBuilder: (context, _) =>
Icon(
Icons.star,
color: Colors.amber,
),
onRatingUpdate: (rating) {
print(rating);
},
),
Text(products[index]
.ratingCount!
.toDouble()
.toString()),
],
),
],
),
),
),
),
),
),
);
},
),
),
],
),
);
});
}
List<Tab> _tabs(List<CategoryModel> categoryModel) {
List<Tab> tabs = [];
for (CategoryModel category in categoryModel) {
tabs.add(Tab(
text: category.name,
));
}
return tabs;
}
وهذه كامل الملفات , ملف stream_products.dart
import 'dart:async';
import '../../common/api_service.dart';
import '../../pages/contracts/contracts.dart';
import '../../models/Product.dart';
class StreamProductsBloc implements Disposable
{
List<ProductsModel>? products;
final StreamController<List<ProductsModel>> _productsController = StreamController<List<ProductsModel>>.broadcast();
final StreamController<int> _categoryController = StreamController<int>.broadcast();
Stream<List<ProductsModel>> get productsStream => _productsController.stream;
StreamSink<int> get fetchProducts => _categoryController.sink;
Stream<int> get getProductsForCategoriescategory => _categoryController.stream;
APIService? apiService;
int categoryID = 0;
StreamProductsBloc()
{
//this.categoryID = categoryID;
this.products = [];
apiService = APIService();
_productsController.add(products!);
_categoryController.add(this.categoryID);
_categoryController.stream.listen(_fetchCategoriesFromApi);
}
Future<void> _fetchCategoriesFromApi(int category) async {
this.products = await apiService!.getProductsForCategories(category);
_productsController.add(this.products!);
}
@override
void dispose(){
_productsController.close();
_categoryController.close();
}
}
و ملف الواجهة ملف products_screen.dart
import 'package:bistore/providers/auth.dart';
import 'package:bistore/services/wishlist_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
import '../../models/init_model.dart';
import '../../widgets/shimmer/shimmer_grid.dart';
import '../../generated/l10n.dart';
import '../../models/cart_model.dart';
import '../products/single_product.dart';
import '../../models/Product.dart';
import '../../models/category.dart';
import '../../utils/helper_api.dart';
import '../../pages/home/stream_products.dart';
class Products extends StatefulWidget {
const Products({Key? key}) : super(key: key);
@override
_ProductsState createState() => _ProductsState();
}
class _ProductsState extends State<Products>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
@override
bool get wantKeepAlive => true;
HelperApi helperApi = new HelperApi();
int currentIndex = 0;
late TabController _tabController;
var refreshKey = GlobalKey<RefreshIndicatorState>();
StreamProductsBloc streamProductsBloc = StreamProductsBloc();
late List<CategoryModel> productsCategories;
@override
void initState() {
// ignore: todo
// TODO: implement initState
super.initState();
HelperApi().fetchCategories(1);
}
@override
void dispose() {
// ignore: todo
// TODO: implement dispose
super.dispose();
_tabController.dispose();
streamProductsBloc.dispose();
}
@override
// ignore: must_call_super
Widget build(BuildContext context) {
return FutureBuilder<List<CategoryModel>>(
future: helperApi.fetchCategories(1),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Center(
child: Text('No Connection'),
);
case ConnectionState.waiting:
case ConnectionState.active:
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
case ConnectionState.done:
if (snapshot.hasError) {
print(snapshot.error);
} else {
if (!snapshot.hasData) {
print('Data Not Found');
} else {
this.productsCategories = snapshot.data;
streamProductsBloc.fetchProducts
.add(this.productsCategories[0].id!);
return _categories(snapshot.data, context);
}
}
break;
}
return Container();
},
);
}
Widget _categories(List<CategoryModel> categoryModel, BuildContext context) {
_tabController = TabController(
initialIndex: 0,
length: categoryModel.length,
vsync: this,
);
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).products),
bottom: TabBar(
indicatorColor: Color(0XFF117182),
indicatorWeight: 5,
controller: _tabController,
tabs: _tabs(categoryModel),
isScrollable: true,
onTap: (int index) {
streamProductsBloc.fetchProducts
.add(this.productsCategories[index].id!);
},
),
),
body: Container(
child: StreamBuilder(
stream: streamProductsBloc.productsStream,
builder: (context, AsyncSnapshot<List<ProductsModel>>? snapshot) {
switch (snapshot!.connectionState) {
case ConnectionState.none:
// ignore: todo
// TODO: Handle this case.
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.waiting:
// ignore: todo
// TODO: Handle this case.
return ShimmerGrid();
case ConnectionState.done:
case ConnectionState.active:
// ignore: todo
// TODO: Handle this case.
if (snapshot.hasError) {
return Center(
child: ShimmerGrid(),
);
} else {
if (!snapshot.hasData) {
return ShimmerGrid();
} else {
return _drawProducts(snapshot.data!, context);
}
}
}
},
),
),
);
}
Widget _drawProducts(List<ProductsModel> products, BuildContext context) {
final cart = Provider.of<Cart>(context, listen: false);
var initModel = Provider.of<InitModel>(context, listen: false,);
var auth = Provider.of<AuthProvider>(context, listen: false);
return Consumer<WishListProvider>(
builder: (BuildContext context, wishListData, child) {
return Container(
padding: EdgeInsets.only(top: 24),
child: Column(
children: [
Flexible(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 1 / 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20),
itemCount: products.length,
itemBuilder: (context, index) {
return Card(
clipBehavior: Clip.hardEdge,
elevation: 0.9,
child: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () {
_gotoSingleProduct(products[index], context);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
// ignore: unnecessary_null_comparison
child: Stack(
children: [
(products[index].images![0].src != null)
? Image.network(
products[index].images![0].src!,
width:
MediaQuery
.of(context)
.size
.width,
height: MediaQuery
.of(context)
.size
.height,
fit: BoxFit.cover,
)
: Image.asset(
"assets/images/1.jpeg",
width:
MediaQuery
.of(context)
.size
.width,
height: MediaQuery
.of(context)
.size
.height,
fit: BoxFit.fill,
),
Positioned.directional(
textDirection: Directionality.of(
context),
child: Container(
margin: EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 10.0),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius:
BorderRadius.circular(50.0)),
child: IconButton(
icon: Icon(
Icons.favorite_outline),
onPressed: () {
//wishListData.getCount(products[index].id!, auth.id!);
wishListData.addNewWishList(
price: products[index]
.price!
.toString(),
product: products[index]
.name!,
image: products[index]
.images![0]
.src!,
productId: products[index]
.id,
userId: auth.id);
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
S
.of(context)
.add_product_to_wishlist,
),
duration: Duration(
seconds: 2),
),
);
},
iconSize: 20.0,
),
),
)
],
)
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
products[index].name!.substring(0, 10),
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(initModel.currency! +
products[index].price!,
style: TextStyle(
color: Colors.black,
fontSize: 15.0,
),
),
),
IconButton(
icon: Icon(
Icons.add_shopping_cart_outlined),
onPressed: () {
//if(auth.status == Status.Authenticated){
cart.addItem(
products[index].id.toString(),
10.0,
products[index].name!.toString(),
products[index].images![0].src!,
1,
products[index].id.toString());
ScaffoldMessenger.of(context)
.hideCurrentSnackBar();
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
S
.of(context)
.product_added_to_the_cart,
),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: S
.of(context)
.undo,
onPressed: () {
cart.reomveSingleItem(
products[index]
.id
.toString());
},
),
),
);
},
),
],
),
Text(products[index].stockStatus!,
style: TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 15,
),),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
RatingBar.builder(
initialRating: products[index]
.ratingCount!
.toDouble(),
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: true,
itemSize: 15.0,
itemCount: 5,
itemPadding:
EdgeInsets.symmetric(horizontal: 4.0),
itemBuilder: (context, _) =>
Icon(
Icons.star,
color: Colors.amber,
),
onRatingUpdate: (rating) {
print(rating);
},
),
Text(products[index]
.ratingCount!
.toDouble()
.toString()),
],
),
],
),
),
),
),
),
),
);
},
),
),
],
),
);
});
}
List<Tab> _tabs(List<CategoryModel> categoryModel) {
List<Tab> tabs = [];
for (CategoryModel category in categoryModel) {
tabs.add(Tab(
text: category.name,
));
}
return tabs;
}
void _gotoSingleProduct(
ProductsModel singleProductModel, BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SingleProduct(singleProductModel)),
);
}
}
class ProductsList extends StatelessWidget {
final List<ProductsModel> productsModel;
ProductsList({Key? key, required this.productsModel}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: productsModel.length,
itemBuilder: (context, index) {
return Text('${productsModel[index].name}');
},
),
),
],
);
}
}