صفحة التصنيفات

  • عندك كروت تصنيفات قادمة من ملف data
  • وعندك منتجات، كل منتج فيه حقل اسم التصنيف (مثلاً "Phones", "Clothes"…)
  • عند الضغط على كارد التصنيف → نذهب لصفحة تعرض فقط المنتجات التابعة لهذا التصنيف

نمشي كذا:

  1. شكل البيانات (Category + Product)
  2. كيف ننتقل بالـ Navigator ونرسل اسم التصنيف
  3. كيف صفحة التصنيف تستقبل الاسم وتفلتر المنتجات
  4. كيف نعرضها في 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,
  });
}
JavaScript
2️⃣ من كارد التصنيف → الانتقال لصفحة التصنيف مع تمرير الاسم

افترض عندك صفحة رئيسية فيها 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

ما في شيء اسمه “نحمّل الروات” زيادة،
إنت بس:

  1. ترسل اسم التصنيف للصفحة الجديدة.
  2. هناك تستخدمه كفلتر على قائمة المنتجات اللي عندك أصلاً في ملف الـ data.
final List<Product> filteredProducts = productsData
    .where(
      (product) => product.category == categoryTitle,
    )
    .toList();
JavaScript

أولاً: الجزء اللي على اليسار
final List<Product> filteredProducts =
JavaScript
  • final
    يعني المتغير مرّة وحدة نعطيه قيمة، وما نقدر نعيد إسناد قيمة ثانية له بعد ذلك.
    (لكن ممكن نعدّل على محتوى القائمة نفسها لو حبّينا؛ المهم ما نغيّر المرجع)
  • List<Product>
    هذا نوع المتغير:
    • List = قائمة (List) في Dart.
    • <Product> = القائمة تحتوي عناصر من نوع Product فقط.
  • filteredProducts
    اسم المتغير، وهو القائمة الجديدة اللي ستحتوي “المنتجات المفلترة حسب التصنيف”.
ثانيًا: المصدر: productsData
productsData
JavaScript

هنا نفترض عندك سابقًا متغيّر مثل:

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 منتجات:

  1. product.category = 'Phones' → الشرط Phones == Phonestrue
  2. product.category = 'Clothes'Clothes == Phonesfalse
  3. product.category = 'Phones'true
  4. product.category = 'Watches'false

إذن بعد where سنحتفظ فقط بالمنتج 1 و 3.

خامسًا: نتيجة where قبل toList()

الدالة where(...) لا ترجع List، بل ترجع:

Iterable<Product>
JavaScript

يعني تركيب يمكنه إنتاج عناصر بالتسلسل، لكنه ليس قائمة List مباشرة.

سادسًا: التحويل إلى List بـ toList()
.where(
  (product) => product.category == categoryTitle,
)
.toList();
JavaScript
  • toList()
    دالة تُحوّل أي Iterable إلى List.

فبعد هذا الاستدعاء:

  • قبل toList() → النوع: Iterable<Product>
  • بعد toList() → النوع: List<Product>

وهذا يطابق نوع المتغير اللي عرفناه:

final List<Product> filteredProducts = ...
JavaScript
تجميع كل شيء معًا بشكل مفهوم

السطر كاملًا يعني:

أنشئ قائمة جديدة اسمها filteredProducts تحتوي فقط المنتجات من productsData التي يكون فيها product.category مساويًا لقيمة categoryTitle.

بكلمات Dart:

  1. نأخذ productsData
  2. نمر على كل product فيها
  3. نختار فقط اللي product.category == categoryTitle
  4. نجمعهم في 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
                                      )
                                ),
                              );
                            },
JavaScript
import '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