تخطيطات Flutter والتصميم المتجاوب

GridView وتخطيطات الشبكة

50 دقيقة الدرس 7 من 16

مقدمة عن GridView

عنصر GridView يعرض العناصر في شبكة ثنائية الأبعاد. هو في الأساس قائمة قابلة للتمرير من العناصر مرتبة في صفوف وأعمدة. يوفر Flutter عدة منشئات محسّنة لحالات استخدام مختلفة، من الشبكات البسيطة ذات الأعمدة الثابتة إلى التخطيطات الديناميكية التي تتكيف مع حجم الشاشة.

مفهوم أساسي: GridView هو عنصر قابل للتمرير يرتب العناصر الفرعية في نمط شبكي. مثل ListView، يتمرر في اتجاه واحد (عمودياً بشكل افتراضي) ويدعم العرض الكسول من خلال منشئات builder.

GridView.count

منشئ GridView.count ينشئ شبكة بعدد ثابت من الأعمدة (البلاطات لكل صف). هذه أبسط طريقة لإنشاء شبكة عندما تعرف بالضبط عدد الأعمدة التي تريدها.

مثال GridView.count

GridView.count(
  crossAxisCount: 3,  // 3 أعمدة
  mainAxisSpacing: 8.0,   // المسافة العمودية بين العناصر
  crossAxisSpacing: 8.0,  // المسافة الأفقية بين العناصر
  padding: const EdgeInsets.all(16.0),
  children: List.generate(12, (index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.blue.shade100,
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Center(
        child: Text(
          'عنصر \${index + 1}',
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
      ),
    );
  }),
)
تحذير: مثل منشئ ListView الافتراضي، يبني GridView.count جميع العناصر مسبقاً. للشبكات الكبيرة، استخدم GridView.builder بدلاً من ذلك.

GridView.extent

منشئ GridView.extent ينشئ شبكة حيث يكون لكل بلاطة حد أقصى للمدى العرضي. يحسب Flutter عدد الأعمدة تلقائياً بناءً على العرض المتاح والحد الأقصى للمدى الذي تحدده. هذا يجعلها متجاوبة بطبيعتها.

مثال GridView.extent

// كل بلاطة بعرض أقصى 150 بكسل
// على شاشة 400 بكسل: عمودان (200 بكسل لكل منهما)
// على شاشة 600 بكسل: 4 أعمدة (150 بكسل لكل منهما)
GridView.extent(
  maxCrossAxisExtent: 150.0,
  mainAxisSpacing: 12.0,
  crossAxisSpacing: 12.0,
  padding: const EdgeInsets.all(16.0),
  children: categories.map((category) {
    return Card(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(category.icon, size: 32.0, color: Colors.blue),
          const SizedBox(height: 8.0),
          Text(
            category.name,
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 12.0),
          ),
        ],
      ),
    );
  }).toList(),
)
نصيحة: استخدم GridView.extent عندما تريد شبكة متجاوبة تضبط عدد الأعمدة تلقائياً بناءً على المساحة المتاحة. هذا مفيد بشكل خاص للشبكات التي يجب أن تبدو جيدة على الهواتف والأجهزة اللوحية.

GridView.builder

منشئ GridView.builder ينشئ عناصر الشبكة بشكل كسول، يبني فقط المرئية على الشاشة. هذا هو النهج الموصى به للشبكات الكبيرة أو المحملة ديناميكياً. يتطلب SliverGridDelegate للتحكم في تخطيط الشبكة.

GridView.builder مع SliverGridDelegateWithFixedCrossAxisCount

// شبكة كتالوج المنتجات
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 16.0,
    crossAxisSpacing: 16.0,
    childAspectRatio: 0.75,  // نسبة العرض / الارتفاع
  ),
  padding: const EdgeInsets.all(16.0),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Image.network(
              product.imageUrl,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 4.0),
                Text(
                  '\$\${product.price.toStringAsFixed(2)}',
                  style: TextStyle(
                    color: Colors.green.shade700,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  },
)

خيارات SliverGridDelegate

مفوّض الشبكة يتحكم في كيفية تحجيم وتموضع العناصر. يوفر Flutter مفوّضَين مدمجَين:

مقارنة مفوّض الشبكة

// SliverGridDelegateWithFixedCrossAxisCount
// عدد أعمدة ثابت - العناصر تتمدد لملء المساحة المتاحة
const SliverGridDelegateWithFixedCrossAxisCount(
  crossAxisCount: 3,          // بالضبط 3 أعمدة
  mainAxisSpacing: 8.0,       // الفجوة العمودية
  crossAxisSpacing: 8.0,      // الفجوة الأفقية
  childAspectRatio: 1.0,      // بلاطات مربعة (عرض/ارتفاع = 1)
)

// SliverGridDelegateWithMaxCrossAxisExtent
// أعمدة ديناميكية بناءً على أقصى عرض للبلاطة
const SliverGridDelegateWithMaxCrossAxisExtent(
  maxCrossAxisExtent: 200.0,  // كل بلاطة بعرض أقصى 200 بكسل
  mainAxisSpacing: 8.0,
  crossAxisSpacing: 8.0,
  childAspectRatio: 1.5,      // أعرض من طولها
  // mainAxisExtent: 120.0,   // بديل: حجم محور رئيسي ثابت
)

شرح childAspectRatio بعمق

خاصية childAspectRatio تتحكم في نسبة عرض كل بلاطة إلى ارتفاعها. فهم هذا أمر حاسم لتصميم شبكات ذات مظهر جيد.

أمثلة childAspectRatio

// childAspectRatio = العرض / الارتفاع

// بلاطات مربعة
childAspectRatio: 1.0     // العرض == الارتفاع

// بلاطات أفقية (أعرض من طولها)
childAspectRatio: 16 / 9  // نسبة الشاشة العريضة
childAspectRatio: 2.0     // ضعف العرض مقارنة بالارتفاع

// بلاطات عمودية (أطول من عرضها)
childAspectRatio: 0.75    // بطاقات المنتجات
childAspectRatio: 0.5     // بطاقات طويلة جداً
childAspectRatio: 2 / 3   // نسبة بورتريه كلاسيكية

// مثال عملي: شبكة منتجات متجاوبة
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 0.7,  // بطاقات طويلة لصور المنتجات + المعلومات
    mainAxisSpacing: 12.0,
    crossAxisSpacing: 12.0,
  ),
  itemCount: 20,
  itemBuilder: (context, index) => const ProductCard(),
)
ملاحظة: إذا لم يمنحك childAspectRatio تحكماً كافياً، استخدم mainAxisExtent على المفوّض لتحديد ارتفاع بكسل دقيق لكل بلاطة بدلاً من ذلك.

مثال عملي: معرض الصور

معرض الصور

class PhotoGalleryScreen extends StatelessWidget {
  final List<String> photoUrls;

  const PhotoGalleryScreen({super.key, required this.photoUrls});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('المعرض')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          // متجاوب: أعمدة أكثر على الشاشات الأوسع
          final crossAxisCount = constraints.maxWidth > 600 ? 4 : 3;

          return GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: crossAxisCount,
              mainAxisSpacing: 4.0,
              crossAxisSpacing: 4.0,
            ),
            padding: const EdgeInsets.all(4.0),
            itemCount: photoUrls.length,
            itemBuilder: (context, index) {
              return GestureDetector(
                onTap: () {
                  // فتح عارض الصور بالشاشة الكاملة
                },
                child: Hero(
                  tag: 'photo_\$index',
                  child: Image.network(
                    photoUrls[index],
                    fit: BoxFit.cover,
                    loadingBuilder: (context, child, loadingProgress) {
                      if (loadingProgress == null) return child;
                      return Container(
                        color: Colors.grey.shade200,
                        child: const Center(
                          child: CircularProgressIndicator(),
                        ),
                      );
                    },
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

مثال عملي: لوحة معلومات

شبكة لوحة المعلومات

class DashboardScreen extends StatelessWidget {
  const DashboardScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final tiles = [
      DashboardTile(
        icon: Icons.shopping_cart,
        title: 'الطلبات',
        value: '156',
        color: Colors.blue,
      ),
      DashboardTile(
        icon: Icons.people,
        title: 'المستخدمون',
        value: '2,340',
        color: Colors.green,
      ),
      DashboardTile(
        icon: Icons.attach_money,
        title: 'الإيرادات',
        value: '\$12.5K',
        color: Colors.orange,
      ),
      DashboardTile(
        icon: Icons.trending_up,
        title: 'النمو',
        value: '+23%',
        color: Colors.purple,
      ),
    ];

    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 200.0,
        mainAxisSpacing: 16.0,
        crossAxisSpacing: 16.0,
        childAspectRatio: 1.2,
      ),
      padding: const EdgeInsets.all(16.0),
      itemCount: tiles.length,
      itemBuilder: (context, index) {
        final tile = tiles[index];
        return Card(
          elevation: 2.0,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(tile.icon, size: 36.0, color: tile.color),
                const SizedBox(height: 8.0),
                Text(
                  tile.value,
                  style: TextStyle(
                    fontSize: 24.0,
                    fontWeight: FontWeight.bold,
                    color: tile.color,
                  ),
                ),
                const SizedBox(height: 4.0),
                Text(
                  tile.title,
                  style: const TextStyle(color: Colors.grey),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
نصيحة: اجمع بين GridView.builder و LayoutBuilder لإنشاء شبكات متجاوبة حقاً. استخدم العرض المتاح من قيود LayoutBuilder لضبط عدد الأعمدة أو أقصى مدى للبلاطات ديناميكياً.