تحسين الأداء

البناء الكسول للقوائم باستخدام ListView.builder والـ Slivers

16 دقيقة الدرس 6 من 12

البناء الكسول للقوائم باستخدام ListView.builder والـ Slivers

من أبرز تحسينات الأداء في Flutter هو التصيير الكسول (Lazy Rendering) — بناء الودجات المرئية على الشاشة فقط، بدلاً من إنشاء جميع عناصر المجموعة مسبقاً. حين تحتوي القائمة على مئات أو آلاف العناصر، فإن بناءها دفعةً واحدة يُهدر الذاكرة ووحدة المعالجة، مما يسبب تقطعاً في الرسوم المتحركة وبطءاً في الانطلاق. يحلّ كلٌّ من ListView.builder وGridView.builder وعائلة Sliver هذه المشكلة بأناقة.

لماذا يضرّ البناء الفوري بالأداء؟

يقبل المُنشئ القياسي لـ ListView قائمة children، حيث يُنشَأ كل ودجت فيها ويُرسم تخطيطه قبل رسم الإطار الأول — حتى العناصر البعيدة عن نطاق الرؤية والتي لن تُرى دون التمرير. هذا مقبول للقوائم الصغيرة والثابتة، لكنه عقبة حقيقية للمجموعات الديناميكية الكبيرة:

  • تُنشأ جميع ودجات العناصر وتُحتفظ بها في الذاكرة في آنٍ واحد.
  • يُحسب التخطيط لكل عنصر بصرف النظر عن مدى رؤيته.
  • تضافة عناصر جديدة تُطلق إعادة بناء كاملة لقائمة children.
  • تتصاعد تقطعات التطبيق بالتناسب مع طول القائمة.
تحذير: لا تمرّر أبداً قائمة List<Widget> مبنيةً بـ .map(...).toList() إلى ListView(children: ...) للقوائم التي تحتوي على أكثر من ~20–30 عنصراً. استخدم ListView.builder بدلاً من ذلك.

ListView.builder — بناء الودجات عند الطلب

يقبل ListView.builder دالة استرداد itemBuilder وعدداً اختيارياً itemCount. تستدعي الدالة فقط المؤشرات التي تتقاطع مع نطاق العرض الحالي، إضافةً إلى امتداد تخزين مؤقت صغير قابل للضبط. تُتلف العناصر التي غادرت الشاشة تلقائياً وتُستعاد ذاكرتها.

ListView.builder — الاستخدام الأساسي

class ContactList extends StatelessWidget {
  final List<Contact> contacts;
  const ContactList({super.key, required this.contacts});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // itemCount يمنع الـ builder من تجاوز نهاية القائمة
      itemCount: contacts.length,

      // itemBuilder يُستدعى بكسل — فقط للمؤشرات المرئية
      itemBuilder: (BuildContext context, int index) {
        final contact = contacts[index];
        return ListTile(
          leading: CircleAvatar(child: Text(contact.initials)),
          title: Text(contact.name),
          subtitle: Text(contact.phone),
        );
      },
    );
  }
}

المعاملات الرئيسية التي تتحكم في الكسل والأداء:

  • itemCount — يُعلم وحدة التحكم بالتمرير بالامتداد الدقيق؛ احذفه فقط للقوائم اللانهائية أو مجهولة الطول.
  • cacheExtent — البكسلات خارج نطاق العرض التي تُبنى مسبقاً (الافتراضي 250 بكسل). زِدها لتمرير أكثر سلاسة؛ قلّلها لتوفير الذاكرة.
  • addRepaintBoundaries — يُغلّف كل عنصر في RepaintBoundary بشكل افتراضي، عازلاً عمليات إعادة الرسم على الأجزاء الفردية.
  • addAutomaticKeepAlives — يُبقي العناصر حيّة إذا احتوت على ودجت من نوع KeepAliveClientMixin.

GridView.builder — شبكات ثنائية الأبعاد كسولة

يطبّق GridView.builder نفس استراتيجية البناء المؤجل على الشبكات. يقبل SliverGridDelegate الذي يتحكم في عدد الأعمدة والتباعد، ودالة itemBuilder التي لا تُستدعى إلا للخلايا الواقعة في المنطقة المرئية.

GridView.builder — شبكة صور

class PhotoGrid extends StatelessWidget {
  final List<String> imageUrls;
  const PhotoGrid({super.key, required this.imageUrls});

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        crossAxisSpacing: 4,
        mainAxisSpacing: 4,
      ),
      itemCount: imageUrls.length,
      itemBuilder: (context, index) {
        return Image.network(
          imageUrls[index],
          fit: BoxFit.cover,
          // عنصر نائب بتأثير ظهور تدريجي أثناء تحميل الصورة
          loadingBuilder: (context, child, loadingProgress) {
            if (loadingProgress == null) return child;
            return const Center(child: CircularProgressIndicator());
          },
        );
      },
    );
  }
}

ودجات Sliver — تخطيط كسول دقيق

السليفر هي الوحدات البدائية القابلة للتمرير على مستوى منخفض في Flutter. كل ListView وGridView هو في الحقيقة غلاف رفيع فوق سليفر داخلياً. يتيح لك استخدام السليفر مباشرةً عبر CustomScrollView تركيب تجارب تمرير معقدة — رؤوس ثابتة، وأشرطة تطبيق قابلة للطي، وأقسام مختلطة من قوائم وشبكات — كلها تشترك في وحدة تحكم تمرير واحدة وميزانية تصيير كسول واحدة.

  • SliverList مع SliverChildBuilderDelegate — المكافئ الكسول لـ ListView.builder.
  • SliverGrid مع مفوّض builder — شبكة ثنائية الأبعاد كسولة داخل عرض تمرير مخصص.
  • SliverAppBar — رأس قابل للطي أو مثبّت يشارك في نفس فيزياء التمرير.
  • SliverToBoxAdapter — يُغلّف أي ودجت ذات حجم ثابت (مثل لافتة) داخل CustomScrollView.
  • SliverFixedExtentList — أسرع من SliverList حين تكون جميع العناصر بنفس الارتفاع، لأن التخطيط يتخطى خطوة قياس الارتفاع كلياً.

CustomScrollView مع سليفر مختلطة

class FeedPage extends StatelessWidget {
  final List<String> stories;
  final List<Post> posts;
  const FeedPage({super.key, required this.stories, required this.posts});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        // شريط تطبيق قابل للطي — ضمن ميزانية التمرير
        const SliverAppBar(
          title: Text('Feed'),
          floating: true,
          snap: true,
          expandedHeight: 120,
        ),

        // شريط قصص بارتفاع ثابت (غير كسول، عدد صغير)
        SliverToBoxAdapter(
          child: SizedBox(
            height: 80,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: stories.length,
              itemBuilder: (_, i) => StoryAvatar(url: stories[i]),
            ),
          ),
        ),

        // قائمة منشورات كسولة — يُبنى العناصر المرئية فقط
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) => PostCard(post: posts[index]),
            childCount: posts.length,
          ),
        ),
      ],
    );
  }
}
نصيحة: حين تكون جميع العناصر في SliverList بارتفاع معروف ومتطابق، انتقل إلى SliverFixedExtentList واضبط itemExtent. يستطيع Flutter حينئذٍ القفز مباشرةً إلى أي إزاحة تمرير دون المرور بكامل القائمة — وهو مكسب كبير للقوائم التي تضم آلاف العناصر.

المفاتيح وهوية العنصر في القوائم الكسولة

نظراً لأن العناصر تُنشأ وتُتلف مع تمرير المستخدم، يجب أن يتمكن Flutter من مقارنة شجرة الودجات الجديدة بالقديمة بكفاءة. يمنع تقديم مفتاح ثابت مشتقٍ من بياناتك (مثل ValueKey(contact.id)) عمليات إعادة البناء غير الضرورية عند إعادة ترتيب القائمة، ويضمن احتفاظ الأطفال ذوي الحالة (الرسوم المتحركة، حقول النص) بحالتهم عبر أحداث التمرير.

الخلاصة الرئيسية: استخدم ListView.builder أو GridView.builder لأي مجموعة تحتوي على أكثر من عدد ضئيل من العناصر. انتقل إلى CustomScrollView مع مفوّضي Sliver حين تحتاج تركيب أقسام تمرير متعددة أو رؤوس ثابتة أو أشرطة تطبيق قابلة للطي. عيّن ValueKey ثابتة للعناصر ذات الحالة للحفاظ على حالتها عبر التمرير. تضمن هذه التقنيات مجتمعةً أن تظل واجهات المستخدم القابلة للتمرير سلسة وفعّالة في استخدام الذاكرة على أي نطاق.