- عندك كروت تصنيفات قادمة من ملف data
- وعندك منتجات، كل منتج فيه حقل اسم التصنيف (مثلاً
"Phones","Clothes"…) - عند الضغط على كارد التصنيف → نذهب لصفحة تعرض فقط المنتجات التابعة لهذا التصنيف
نمشي كذا:
- شكل البيانات (Category + Product)
- كيف ننتقل بالـ Navigator ونرسل اسم التصنيف
- كيف صفحة التصنيف تستقبل الاسم وتفلتر المنتجات
- كيف نعرضها في Grid/List
1️⃣ شكل البيانات (افتراض بسيط يشبه اللي عندك)
🔹 ملف التصنيفات: categories_data.dart
class Category {
final String name; // نفس الاسم اللي موجود داخل المنتج
final String image; // صورة الكارد مثلاً
const Category({
required this.name,
required this.image,
});
}
const List<Category> categories = [
Category(
name: 'Phones',
image: 'assets/images/cat_phones.png',
),
Category(
name: 'Clothes',
image: 'assets/images/cat_clothes.png',
),
Category(
name: 'Watches',
image: 'assets/images/cat_watches.png',
),
];
JavaScript🔹 ملف المنتجات: products_data.dart
import 'productModel.dart';
final List<Product> products = [
Product(
title: 'iPhone 15',
image: 'assets/images/iphone15.png',
description: 'أحدث آيفون...',
price: 5000,
categoryName: 'Phones', // 👈 مهم جداً يطابق اسم التصنيف
),
Product(
title: 'قميص رجالي',
image: 'assets/images/shirt.png',
description: 'قميص قطن 100%',
price: 120,
categoryName: 'Clothes',
),
// ...
];JavaScript🔹 موديل المنتج: productModel.dart (المهم هنا حقل التصنيف)
class Product {
final String title;
final String image;
final String description;
final double price;
// 👇 هذا اللي قلت عليه “حقل اسم التصنيف”
final String categoryName;
const Product({
required this.title,
required this.image,
required this.description,
required this.price,
required this.categoryName,
});
}JavaScript2️⃣ من كارد التصنيف → الانتقال لصفحة التصنيف مع تمرير الاسم
افترض عندك صفحة رئيسية فيها Grid للتصنيفات:
import 'categories_data.dart';
import 'category_products_page.dart';
GridView.builder(
itemCount: categories.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
),
itemBuilder: (context, index) {
final category = categories[index];
return GestureDetector(
onTap: () {
// 👈 هنا الانتقال لصفحة التصنيف وتمرير الاسم
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CategoryProductsPage(
categoryName: category.name, // هذا هو المفتاح
),
),
);
},
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(category.image, height: 60),
const SizedBox(height: 8),
Text(
category.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
);
},
);JavaScriptالمهم هنا:
إحنا نمرّر category.name إلى صفحة التصنيف، عشان هناك نستخدمه في الفلترة.
3️⃣ صفحة التصنيف: تستقبل الاسم وتفلتر المنتجات
صفحة جديدة مثلاً: category_products_page.dart
import 'package:flutter/material.dart';
import '../data/products_data.dart';
import '../models/productModel.dart';
import 'product_details_page.dart';
class CategoryProductsPage extends StatelessWidget {
final String categoryName; // 👈 هذا اللي جاي من الكارد
const CategoryProductsPage({
super.key,
required this.categoryName,
});
@override
Widget build(BuildContext context) {
// ✅ هنا الفلترة المهمة
final List<Product> filteredProducts = products
.where((product) => product.categoryName == categoryName)
.toList();
return Scaffold(
appBar: AppBar(
title: Text(categoryName), // بس نعرض الاسم في العنوان
),
body: filteredProducts.isEmpty
? const Center(
child: Text('لا توجد منتجات في هذا التصنيف'),
)
: GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: filteredProducts.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (context, index) {
final product = filteredProducts[index];
return GestureDetector(
onTap: () {
// فتح صفحة تفاصيل المنتج
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProductDetailsPage(
productDetails: product,
),
),
);
},
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Expanded(
child: Image.asset(
product.image,
width: double.infinity,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
product.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'${product.price.toStringAsFixed(2)} ر.س',
style: const TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
],
),
),
);
},
),
);
}
}
JavaScriptأهم سطرين في الموضوع كله:
// 1) استقبال اسم التصنيف:
final String categoryName;
// 2) فلترة المنتجات:
final List<Product> filteredProducts = products
.where((product) => product.categoryName == categoryName)
.toList();JavaScriptأي منتج يملك
categoryNameيساوي الاسم اللي أرسلناه من الكارد
سيتم عرضه في هذه الصفحة. الباقي يُتجاهل 🚫
4️⃣ ماذا عن “الروت” (Routes)؟
لو تستخدم named routes بدل MaterialPageRoute، نفس الفكرة، بس تمرّر الباراميتر في arguments وتستقبله في onGenerateRoute.
بس بما إنك حالياً تستخدم push عادي، هذا الشكل أبسط عليك:
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CategoryProductsPage(categoryName: category.name),
),
);
JavaScriptما في شيء اسمه “نحمّل الروات” زيادة،
إنت بس:
- ترسل اسم التصنيف للصفحة الجديدة.
- هناك تستخدمه كفلتر على قائمة المنتجات اللي عندك أصلاً في ملف الـ data.
final List<Product> filteredProducts = productsData
.where(
(product) => product.category == categoryTitle,
)
.toList();JavaScriptأولاً: الجزء اللي على اليسار
final List<Product> filteredProducts =JavaScriptfinal
يعني المتغير مرّة وحدة نعطيه قيمة، وما نقدر نعيد إسناد قيمة ثانية له بعد ذلك.
(لكن ممكن نعدّل على محتوى القائمة نفسها لو حبّينا؛ المهم ما نغيّر المرجع)List<Product>
هذا نوع المتغير:List= قائمة (List) في Dart.<Product>= القائمة تحتوي عناصر من نوعProductفقط.
filteredProducts
اسم المتغير، وهو القائمة الجديدة اللي ستحتوي “المنتجات المفلترة حسب التصنيف”.
ثانيًا: المصدر: productsData
productsDataJavaScriptهنا نفترض عندك سابقًا متغيّر مثل:
List<Product> productsData = [...];JavaScriptيعني:
productsDataهي القائمة الأصلية اللي تحتوي كل المنتجات.- نحن لا نريد كل المنتجات الآن، بل فقط المنتجات التي تنتمي لتصنيف معيّن.
ثالثًا: دالة where(...)
productsData.where(
(product) => product.category == categoryTitle,
)JavaScriptما هي where؟
whereهي دالة موجودة على أيIterable(مثل List).- تأخذ دالة شرطية (تسمى
predicate) وتُرجِع:Iterable<Product>يحتوي فقط العناصر اللي تحقق الشرط.
يعني:
مر على كل منتج في
productsData
واحتفظ فقط بالمنتجات اللي ترجعtrueمن الدالة الشرطية.
رابعًا: الدالة الشرطية (product) => ...
(product) => product.category == categoryTitle,JavaScriptهذا يسمّى Lambda أو Arrow Function.
تفصيلها:
(product)
باراميتر واحد يمثل كل عنصر يتم المرور عليه من القائمة.
نوعه هنا هوProduct، لأنproductsDataهيList<Product>.=>
اختصار لـ:(product) { return product.category == categoryTitle; }يعني دالة ترجع قيمة (هناbool) بسطر واحد.product.category == categoryTitle
هذا هو الشرط:product.category:
حقل التصنيف داخل المنتج الواحد. مثلاً:"Phones","Clothes","Watches".categoryTitle:
المتغيّر اللي جاي من صفحة التصنيفات أو من الكارد، ويمثّل اسم التصنيف المطلوب عرضه.==
عامل مقارنة في Dart:
يرجعtrueلو القيمتين متساويتين، وfalseإذا اختلفتا.
إذن:
لو كان تصنيف هذا المنتج (
product.category)
يساوي اسم التصنيف المطلوب (categoryTitle)
→ ترجع الدالةtrue
→ يبقيه في النتيجة
لو غير متساوي →false→ يتم تجاهله.
مثال عملي:
نفترض:
categoryTitle = 'Phones';JavaScriptو productsData فيها 4 منتجات:
product.category = 'Phones'→ الشرطPhones == Phones→true✅product.category = 'Clothes'→Clothes == Phones→false❌product.category = 'Phones'→true✅product.category = 'Watches'→false❌
إذن بعد where سنحتفظ فقط بالمنتج 1 و 3.
خامسًا: نتيجة where قبل toList()
الدالة where(...) لا ترجع List، بل ترجع:
Iterable<Product>JavaScriptيعني تركيب يمكنه إنتاج عناصر بالتسلسل، لكنه ليس قائمة List مباشرة.
سادسًا: التحويل إلى List بـ toList()
.where(
(product) => product.category == categoryTitle,
)
.toList();JavaScripttoList()
دالة تُحوّل أيIterableإلىList.
فبعد هذا الاستدعاء:
- قبل
toList()→ النوع:Iterable<Product> - بعد
toList()→ النوع:List<Product>
وهذا يطابق نوع المتغير اللي عرفناه:
final List<Product> filteredProducts = ...JavaScriptتجميع كل شيء معًا بشكل مفهوم
السطر كاملًا يعني:
أنشئ قائمة جديدة اسمها
filteredProductsتحتوي فقط المنتجات منproductsDataالتي يكون فيهاproduct.categoryمساويًا لقيمةcategoryTitle.
بكلمات Dart:
- نأخذ
productsData - نمر على كل
productفيها - نختار فقط اللي
product.category == categoryTitle - نجمعهم في
List<Product>جديدة اسمهاfilteredProducts.
مثال صغير توضيحي بالأرقام بدل المنتجات
لو عندك:
final numbers = [1, 2, 3, 4, 5, 6];
final evenNumbers = numbers
.where((n) => n % 2 == 0)
.toList();JavaScript- هنا الشرط:
(n) => n % 2 == 0
يعني اختر فقط الأعداد الزوجية. - النتيجة:
evenNumbers = [2, 4, 6];
نفس الفكرة بالضبط، بس بدل الأرقام عندك منتجات وبدل الزوجي/الفردي عندك تطابق/عدم تطابق التصنيف.
InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
CategoryPage(
categoryTitle: item.title
)
),
);
},JavaScriptimport 'package:first_store/data/productsData.dart';
import 'package:first_store/models/productModel.dart';
import 'package:first_store/screens/productDetails.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
class CategoryPage extends StatelessWidget {
final String categoryTitle;
CategoryPage({required this.categoryTitle});
@override
Widget build(BuildContext context) {
final List<Product> filteredProducts = productsData
.where(
(product) => product.category == categoryTitle,
)
.toList();
return Scaffold(
appBar: AppBar(title: Text("${categoryTitle}"),),
body: Center(
child: Padding(
padding: EdgeInsets.all(10),
child: Container(
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"أحدث المنتجات",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
"${categoryTitle}",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text("كافة المنتجات"),
],
),
Container(
height: 600,
child:
filteredProducts.isEmpty
? const Center(
child: Text('لا توجد منتجات في هذا التصنيف'),
)
:
GridView.builder(
itemCount: filteredProducts.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisExtent: 330,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemBuilder: (context, i) {
final item = filteredProducts[i];
return InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
ProductDetailsPage(
productDetails: item,
),
),
);
},
child: Container(
child: Card(
clipBehavior: Clip.hardEdge,
child: Column(
children: [
Image.asset(
item.image,
height: 160,
width: double.infinity,
fit: BoxFit.cover,
),
Gap(10),
Text(
item.title,
maxLines: 2,
style: TextStyle(
fontSize: 16,
fontWeight:
FontWeight.bold,
),
textAlign:
TextAlign.center,
),
Padding(
padding:
const EdgeInsets.all(
10,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(
"${item.price}\$",
style: TextStyle(
fontSize: 14,
fontWeight:
FontWeight
.bold,
color: Colors.red,
),
),
Text(item.category),
],
),
),
Padding(
padding:
const EdgeInsets.all(
10.0,
),
child: Text(
item.description,
maxLines: 2,
overflow: TextOverflow
.ellipsis,
textAlign:
TextAlign.center,
),
),
],
),
),
),
);
},
),
),
],
),
),
),
),
);
}
}
JavaScript