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

SliverAppBar و SliverList و SliverGrid

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

مقدمة إلى Slivers

Slivers هي اللبنات الأساسية للمناطق القابلة للتمرير في Flutter. بينما تُعد عناصر مثل ListView و GridView مريحة، إلا أنها في الحقيقة مبنية فوق slivers. عندما تحتاج إلى تأثيرات تمرير متقدمة — رؤوس قابلة للطي، عناصر ثابتة، أو تخطيطات تمرير مختلطة — فإنك تعمل مباشرة مع slivers داخل CustomScrollView.

مفهوم أساسي: الـ sliver هو جزء من منطقة قابلة للتمرير. كلمة “sliver” تعني شريحة رقيقة — كل sliver يتعامل فقط مع جزئه الخاص من منفذ العرض. CustomScrollView يجمع عدة slivers في سطح تمرير موحد.

أساسيات CustomScrollView

عنصر CustomScrollView يأخذ قائمة من slivers في خاصية slivers. كل عنصر فرعي يجب أن يكون عنصر sliver (يبدأ بـ Sliver). لا يمكنك خلط العناصر العادية والـ slivers مباشرة.

هيكل CustomScrollView الأساسي

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // عناصر Sliver تُوضع هنا
          const SliverAppBar(
            title: Text('تطبيقي'),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                title: Text('عنصر \$index'),
              ),
              childCount: 50,
            ),
          ),
        ],
      ),
    );
  }
}

SliverAppBar

SliverAppBar هو أحد أقوى عناصر sliver. ينشئ شريط تطبيق يمكنه التوسع والطي والعوم والتثبيت والالتقاط أثناء تمرير المستخدم. يحل محل AppBar القياسي عند استخدامه داخل CustomScrollView.

الخصائص الرئيسية

  • expandedHeight — ارتفاع شريط التطبيق عند التوسع الكامل.
  • floating — إذا كان true، يظهر شريط التطبيق بمجرد أن يمرر المستخدم للأعلى، حتى لو لم يصل إلى القمة.
  • pinned — إذا كان true، يبقى شريط التطبيق المطوي مرئيًا في الأعلى.
  • snap — إذا كان true (يتطلب floating: true)، ينفتح شريط التطبيق أو ينغلق بالكامل بدلاً من الظهور الجزئي.
  • flexibleSpace — عنصر يتمدد لملء المنطقة الموسعة، عادةً FlexibleSpaceBar.

SliverAppBar مع FlexibleSpaceBar

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 250.0,
            floating: false,
            pinned: true,
            snap: false,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('منظر الجبل'),
              centerTitle: true,
              background: Image.network(
                'https://picsum.photos/800/400',
                fit: BoxFit.cover,
              ),
              collapseMode: CollapseMode.parallax,
            ),
            actions: [
              IconButton(
                icon: const Icon(Icons.share),
                onPressed: () {},
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                leading: CircleAvatar(child: Text('\${index + 1}')),
                title: Text('عنصر \${index + 1}'),
                subtitle: const Text('نص فرعي'),
              ),
              childCount: 30,
            ),
          ),
        ],
      ),
    );
  }
}
نصيحة: استخدم CollapseMode.parallax لتأثير التمرير المتوازي على صورة الخلفية، CollapseMode.pin لإبقاء الخلفية ثابتة، أو CollapseMode.none لعدم وجود سلوك خاص.

العوم والالتقاط

خصائص floating و snap تعمل معًا للتحكم في كيفية ظهور شريط التطبيق عند التمرير للأعلى في منتصف القائمة.

SliverAppBar عائم مع التقاط

SliverAppBar(
  expandedHeight: 200.0,
  floating: true,
  pinned: false,
  snap: true,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('شريط الوصول السريع'),
    background: Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Colors.deepPurple, Colors.indigo],
        ),
      ),
    ),
  ),
)
تحذير: تعيين snap: true يتطلب floating: true. إذا عيّنت snap: true بدون floating: true، سيطرح Flutter خطأ تأكيد في وقت التشغيل.

SliverList

SliverList يعرض قائمة خطية من العناصر الفرعية داخل CustomScrollView. يوفر Flutter 3.x مُنشئات مريحة تبسّط الإنشاء.

SliverList.builder

المُنشئ الأكثر شيوعًا، مكافئ لـ ListView.builder. يبني العناصر الفرعية كسولًا عند الطلب.

مثال SliverList.builder

final List<String> cities = [
  'لندن', 'باريس', 'طوكيو', 'نيويورك',
  'سيدني', 'دبي', 'برلين', 'روما',
];

SliverList.builder(
  itemCount: cities.length,
  itemBuilder: (context, index) {
    return Card(
      margin: const EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 4,
      ),
      child: ListTile(
        leading: const Icon(Icons.location_city),
        title: Text(cities[index]),
        trailing: const Icon(Icons.chevron_right),
        onTap: () {
          // الانتقال إلى تفاصيل المدينة
        },
      ),
    );
  },
)

SliverList.separated

هذا المُنشئ يضيف عنصر فاصل بين كل عنصر، تمامًا مثل ListView.separated.

مثال SliverList.separated

SliverList.separated(
  itemCount: 20,
  itemBuilder: (context, index) {
    return Padding(
      padding: const EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 8,
      ),
      child: Row(
        children: [
          CircleAvatar(
            radius: 24,
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              '\${index + 1}',
              style: const TextStyle(color: Colors.white),
            ),
          ),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'جهة اتصال \${index + 1}',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
                Text(
                  'contact\${index + 1}@example.com',
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  },
  separatorBuilder: (context, index) => const Divider(
    height: 1,
    indent: 72,
  ),
)

SliverGrid

SliverGrid يرتب العناصر الفرعية في شبكة ثنائية الأبعاد داخل CustomScrollView. تتحكم في تخطيط الشبكة باستخدام gridDelegate.

SliverGrid مع SliverGridDelegateWithFixedCrossAxisCount

SliverGrid(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 8.0,
    crossAxisSpacing: 8.0,
    childAspectRatio: 1.0,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      return Card(
        color: Colors.primaries[index % Colors.primaries.length],
        child: Center(
          child: Text(
            'بلاطة \${index + 1}',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      );
    },
    childCount: 20,
  ),
)
نصيحة: استخدم SliverGridDelegateWithMaxCrossAxisExtent عندما تريد أن يكون لكل بلاطة عرض أقصى وتدع Flutter يحسب عدد الأعمدة تلقائيًا. هذا رائع لتخطيطات الشبكة المتجاوبة.

SliverFixedExtentList

عندما يكون لجميع العناصر في القائمة نفس الارتفاع الثابت، فإن SliverFixedExtentList أكثر كفاءة من SliverList لأن Flutter يمكنه حساب مواقع العناصر دون قياس كل عنصر فرعي.

مثال SliverFixedExtentList

SliverFixedExtentList(
  itemExtent: 72.0,
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      return Container(
        alignment: Alignment.centerLeft,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        decoration: BoxDecoration(
          border: Border(
            bottom: BorderSide(
              color: Colors.grey.shade200,
            ),
          ),
        ),
        child: Row(
          children: [
            Icon(
              Icons.music_note,
              color: Colors.blue.shade400,
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Text(
                'مقطع \${index + 1} - عنوان الأغنية',
                style: const TextStyle(fontSize: 16),
              ),
            ),
            Text(
              '3:\${(index * 7 % 60).toString().padLeft(2, '0')}',
              style: TextStyle(color: Colors.grey[600]),
            ),
          ],
        ),
      );
    },
    childCount: 100,
  ),
)

SliverAnimatedList

SliverAnimatedList يوفر إدراجات وإزالات متحركة داخل CustomScrollView. يعمل مثل AnimatedList لكن كـ sliver.

مثال SliverAnimatedList

class AnimatedSliverListPage extends StatefulWidget {
  const AnimatedSliverListPage({super.key});

  @override
  State<AnimatedSliverListPage> createState() =>
      _AnimatedSliverListPageState();
}

class _AnimatedSliverListPageState extends State<AnimatedSliverListPage> {
  final GlobalKey<SliverAnimatedListState> _listKey =
      GlobalKey<SliverAnimatedListState>();
  final List<String> _items = ['تفاح', 'موز', 'كرز'];

  void _addItem() {
    final index = _items.length;
    _items.add('فاكهة \${index + 1}');
    _listKey.currentState?.insertItem(
      index,
      duration: const Duration(milliseconds: 400),
    );
  }

  void _removeItem(int index) {
    final removed = _items.removeAt(index);
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => SizeTransition(
        sizeFactor: animation,
        child: Card(
          color: Colors.red.shade100,
          child: ListTile(title: Text(removed)),
        ),
      ),
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(
            title: Text('قائمة Sliver متحركة'),
            pinned: true,
          ),
          SliverAnimatedList(
            key: _listKey,
            initialItemCount: _items.length,
            itemBuilder: (context, index, animation) {
              return SizeTransition(
                sizeFactor: animation,
                child: Card(
                  margin: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 4,
                  ),
                  child: ListTile(
                    title: Text(_items[index]),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () => _removeItem(index),
                    ),
                  ),
                ),
              );
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addItem,
        child: const Icon(Icons.add),
      ),
    );
  }
}

مثال عملي: تخطيط متجر التطبيقات

لنجمع كل شيء في مثال واقعي — صفحة متجر تطبيقات مع رأس قابل للطي وقسم مميز أفقي وشبكة تطبيقات.

تخطيط متجر التطبيقات الكامل

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // رأس قابل للطي
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('متجر التطبيقات'),
              background: Container(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [Colors.blue, Colors.indigo],
                  ),
                ),
                child: const Center(
                  child: Icon(
                    Icons.store,
                    size: 64,
                    color: Colors.white70,
                  ),
                ),
              ),
            ),
          ),

          // عنوان القسم
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'أفضل التطبيقات',
                style: TextStyle(
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),

          // شبكة التطبيقات
          SliverPadding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverGrid(
              gridDelegate:
                  const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200,
                mainAxisSpacing: 12,
                crossAxisSpacing: 12,
                childAspectRatio: 0.75,
              ),
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    elevation: 2,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircleAvatar(
                          radius: 30,
                          backgroundColor: Colors
                              .primaries[index % Colors.primaries.length],
                          child: const Icon(
                            Icons.apps,
                            color: Colors.white,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'تطبيق \${index + 1}',
                          style: const TextStyle(
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 4),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: List.generate(
                            5,
                            (i) => Icon(
                              Icons.star,
                              size: 14,
                              color: i < 4
                                  ? Colors.amber
                                  : Colors.grey.shade300,
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
                childCount: 12,
              ),
            ),
          ),

          // مسافة سفلية
          const SliverToBoxAdapter(
            child: SizedBox(height: 32),
          ),
        ],
      ),
    );
  }
}
ملخص: تفتح Slivers أنماط تمرير متقدمة في Flutter. SliverAppBar يوفر رؤوس قابلة للطي مع مساحة مرنة. SliverList و SliverGrid يتعاملان مع التخطيطات الخطية والشبكية داخل CustomScrollView. استخدم SliverFixedExtentList للأداء عندما تتشارك العناصر نفس الارتفاع، و SliverAnimatedList للإضافات والإزالات المتحركة. اجمع عدة slivers في CustomScrollView واحد لواجهات تمرير غنية وعالية الأداء.