الرسوم المتحركة وتصميم الحركة

AnimatedBuilder: إعادة البناء بكفاءة

15 دقيقة الدرس 6 من 13

AnimatedBuilder: إعادة البناء بكفاءة

عندما تستخدم AnimationController مباشرةً داخل StatefulWidget، فإن استدعاء setState() عند كل إطار متحرك يُعيد بناء شجرة الودجات بالكامل المرتبطة بهذا الودجت — حتى الأجزاء التي لا علاقة لها بالحركة. يحلّ AnimatedBuilder هذه المشكلة بعزل إعادة البناء على الشجرة الفرعية التي تتغير فعلاً، مع إبقاء بقية التسلسل الهرمي للودجات دون تغيير.

ملاحظة: AnimatedBuilder هو ودجت متعدد الأغراض يستمع إلى أي Listenable (بما في ذلك Animation وAnimationController وChangeNotifier) ويستدعي دالة builder عند كل تغيير. فقط الشجرة الفرعية التي يُعيدها builder تُعاد بناؤها — أما معامل child فيُبنى مرةً واحدة ويُمرَّر دون تغيير.

لماذا لا نستخدم setState فقط؟

تخيّل شاشة معقدة تحتوي على ترويسة وقائمة وأيقونة متحركة صغيرة. إذا كان الودجت الأب يمتلك AnimationController ويستمع إليه عبر addListener(() => setState((){}));، فإن Flutter يُعيد بناء الشاشة بالكامل — الترويسة والقائمة والأيقونة — ستين مرة في الثانية. هذا يُهدر موارد وحدة المعالجة المركزية والمعالج الرسومي. مع AnimatedBuilder، تُعاد بناء الشجرة الفرعية للأيقونة فقط عند كل إطار.

  • فصل المسؤوليات: منطق الحركة يعيش في فئة الحالة؛ المظهر المتحرك يُوصف في builder.
  • الابن القابل لإعادة الاستخدام: الشجرات الفرعية الثقيلة والثابتة تُبنى مرةً واحدة وتُعاد استخدامها عبر الإطارات عبر معامل child.
  • قابلية التركيب: يمكن لـ AnimatedBuilder أن يُغلّف أي ودجت، مما يُسهّل تحريك ودجات الطرف الثالث أو القديمة دون تعديلها.

معامل child — مفتاح الأداء

أهم تحسين هو تمرير ودجت مبني مسبقاً كمعامل child. يبني Flutter هذه الشجرة الفرعية مرةً واحدة، يُخزّنها مؤقتاً، ويُسلّم نفس الكائن لكل استدعاء لـ builder. بدونها، سيُخصّص كل إطار شجرة ودجات جديدة حتى لو كانت هذه الشجرة متطابقة بصرياً عبر الإطارات.

النمط الأساسي لـ AnimatedBuilder

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

  @override
  State<SpinningLogo> createState() => _SpinningLogoState();
}

class _SpinningLogoState extends State<SpinningLogo>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(); // تكرار إلى ما لا نهاية
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // النص أدناه مُكلف البناء — مرّره كـ child
    // حتى يُنشأ مرةً واحدة فقط، لا عند كل إطار.
    return AnimatedBuilder(
      animation: _controller,
      child: const Text(
        'Flutter',
        style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
      ),
      builder: (BuildContext context, Widget? child) {
        // يُستدعى builder ~60 مرة في الثانية.
        // فقط Transform يُعاد بناؤه؛ child يُعاد استخدامه.
        return Transform.rotate(
          angle: _controller.value * 2 * 3.14159,
          child: child, // مُعاد الاستخدام، لا يُعاد بناؤه أبداً
        );
      },
    );
  }
}

تحريك خصائص متعددة

يمكن لـ AnimatedBuilder الواحد قراءة حركات متعددة مشتقة من Tween يُشغّلها نفس المتحكم. سلسل استدعاءات Tween.animate() لإنشاء كائنات Animation<T>، ثم اقرأ قيمها .value داخل builder.

دمج التحجيم والشفافية والانزلاق في Builder واحد

class FancyCard extends StatefulWidget {
  final Widget content;
  const FancyCard({super.key, required this.content});

  @override
  State<FancyCard> createState() => _FancyCardState();
}

class _FancyCardState extends State<FancyCard>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _scale;
  late final Animation<double> _opacity;
  late final Animation<Offset> _slide;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 600),
    );

    _scale = Tween<double>(begin: 0.6, end: 1.0).animate(
      CurvedAnimation(parent: _ctrl, curve: Curves.easeOutBack),
    );
    _opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _ctrl, curve: Curves.easeIn),
    );
    _slide = Tween<Offset>(
      begin: const Offset(0, 0.3),
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));

    _ctrl.forward(); // شغّل مرةً واحدة عند الإضافة
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _ctrl,
      // widget.content ثابت — مرّره كـ child حتى لا
      // يُعاد تكوينه عند كل إطار متحرك.
      child: widget.content,
      builder: (context, child) {
        return FadeTransition(
          opacity: _opacity,
          child: SlideTransition(
            position: _slide,
            child: ScaleTransition(
              scale: _scale,
              child: child, // ودجت المحتوى المبني مسبقاً
            ),
          ),
        );
      },
    );
  }
}
نصيحة: يُوفّر Flutter ودجات مساعدة جاهزة — FadeTransition وScaleTransition وSlideTransition وRotationTransition — وهي أغلفة خفيفة حول AnimatedBuilder للحركات أحادية الخاصية. استخدمها عند تحريك سمة واحدة؛ والجأ إلى AnimatedBuilder مباشرةً عندما تحتاج لقراءة حركات متعددة في دالة builder واحدة أو تريد تحكماً كاملاً فيما يُعاد بناؤه.

فصل المنطق عن واجهة المستخدم باستخدام AnimatedBuilder

نمط معماري نظيف هو الإبقاء على كل إعداد متحكم الحركة والـ Tween داخل فئة State، ووصف المظهر المتحرك كاملاً في دالة AnimatedBuilder.builder. الودجت الأب الذي يحتوي على المحتوى غير المتحرك لا يُعاد بناؤه أبداً. هذا يُبقي المسؤوليات واضحة ويُسهّل اختبار الحركات بمعزل عن باقي الكود.

تحذير: لا تقرأ animation.value داخل setState() وتخزّنه في متغير عضو. هذا يُبطل الغرض من AnimatedBuilder: إذ سيُعيد بناء الودجت بالكامل بدلاً من الشجرة الفرعية المتحركة فقط. اقرأ دائماً animation.value مباشرةً داخل دالة الاستدعاء builder.

AnimatedBuilder مقابل AnimatedWidget

AnimatedWidget هو الوحدة البنائية الأدنى مستوى — إنه فئة فرعية من StatefulWidget تستدعي setState تلقائياً عندما يُطلق Listenable. أما AnimatedBuilder فهو فئة فرعية ملموسة من AnimatedWidget تُضيف ميزة builder + child. من الناحية العملية ستستخدم AnimatedBuilder في كل الحالات تقريباً؛ وتمدّد AnimatedWidget مباشرةً فقط عندما تريد إنشاء فئة ودجت متحرك قابلة لإعادة الاستخدام ومسمّاة.

الخلاصة

AnimatedBuilder هو الأسلوب الاصطلاحي في Flutter لإدارة إعادات البناء الدقيقة والفعّالة من AnimationController. مرّر الجزء الثابت والمُكلف من شجرة الودجات كـ child وسيتأكد Flutter من عدم إعادة بنائه عند إطارات الحركة. استخدم دالة builder لقراءة قيم الحركة وإعادة الغلاف الخفيف الذي يتغير فقط. يُبقي هذا النمط ميزانية الإطارات سليمة وكودك نظيفاً.

النقطة الرئيسية: AnimatedBuilder يُعيد بناء الشجرة الفرعية لـ builder فقط عند كل إطار. مرّر الودجات الثابتة كـ child حتى تُبنى مرةً واحدة وتُعاد استخدامها. احتفظ بكل إعداد Tween والمتحكم في فئة State واقرأ .value حصراً داخل دالة الاستدعاء builder.