عزل الرسم باستخدام RepaintBoundary
عزل الرسم باستخدام RepaintBoundary
يُنفِّذ Flutter عملية عرض واجهة المستخدم عبر مراحل متعاقبة: البناء → التخطيط → الرسم → التركيب. المرحلتان الأخيرتان—الرسم والتركيب—هما حيث تُرسم وحدات البكسل فعلياً على الشاشة. فهم كيفية تجميع Flutter لهذه المراحل وعزلها أمر جوهري لبناء تطبيقات سلسة بمعدل إطارات عالٍ.
RepaintBoundary هو ودجت يوجّه مُركِّب Flutter لوضع الشجرة الفرعية الخاصة به على طبقة تركيب مستقلة. يعني ذلك أنه عندما يُعيد أي شيء داخل هذا الحد رسمَ نفسه، تظل بقية شجرة الودجات غير متأثرة تماماً—تُقرأ من الطبقة المؤقتة مباشرةً دون إعادة تنفيذ عمليات الرسم الخاصة بها.
كيف يستخدم المُركِّب الطبقات
يُشبه مُركِّب Flutter محرك رسوميات يدير مجموعة من الطبقات. كل طبقة عبارة عن صورة نقطية مُرسترة ومُخزَّنة مؤقتاً في وحدة معالجة الرسوميات. عندما تُصنَّف طبقة على أنها متسخة، يُعاد رسترة تلك الطبقة وحدها؛ أما الطبقات النظيفة فتُركَّب من صورها المؤقتة. RepaintBoundary تُنشئ طبقة ورقية جديدة، مما يتيح رسترة شجرتها الفرعية بشكل مستقل.
- بدون RepaintBoundary: الودجت المتحرك يتسبب في تعليم الأب كمتسخ → تُعيد كل الشجرة الشقيقة رسم نفسها
- مع RepaintBoundary: الودجت المتحرك يعيش في طبقته المستقلة → فقط تلك الطبقة تُعاد رسترتها → الودجات الشقيقة تُقرأ من ذاكرة التخزين المؤقتة لوحدة معالجة الرسوميات
- يمزج المُركِّب بعد ذلك جميع الطبقات معاً، وهو أمر رخيص للغاية مقارنةً بإعادة رسترة وحدات البكسل
تصور شجرة الطبقات
يمكنك فحص طبقات التركيب في وقت التشغيل باستخدام مستكشف الطبقات في Flutter DevTools، أو بتفعيل debugRepaintRainbowEnabled = true في دالة main. ستومض الودجات التي تُعيد رسم نفسها في كل إطار بتراكب قوس قزح متدرج، مما يجعل نقاط الاستنزاف واضحة فوراً.
تفعيل التصور البصري لإعادة الرسم
import 'package:flutter/rendering.dart';
void main() {
// إبراز الودجات التي تُعيد رسم نفسها في كل إطار بحدود قوس قزح
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
الاستخدام الأساسي
تغليف ودجت بـ RepaintBoundary بسيط من الناحية النحوية. المهارة تكمن في معرفة أين تضعه لتحقيق أقصى فائدة.
عزل ودجت يتحرك بتكرار عالٍ
import 'package:flutter/material.dart';
class LiveDashboard extends StatelessWidget {
const LiveDashboard({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// محتوى ثابت: يُرسم مرة واحدة ويُخزَّن مؤقتاً في وحدة معالجة الرسوميات
const _ReportSummaryCard(),
// معزول: مؤشر التذبذب الحي يُعيد رسم نفسه في كل إطار
// لكن ReportSummaryCard أعلاه لا يُبطَّل أبداً
RepaintBoundary(
child: _LiveTickerWidget(),
),
// ثابت أيضاً: محمي من إعادات الرسم المستمرة للمؤشر
const _FooterLinks(),
],
);
}
}
class _LiveTickerWidget extends StatefulWidget {
@override
State<_LiveTickerWidget> createState() => _LiveTickerWidgetState();
}
class _LiveTickerWidgetState extends State<_LiveTickerWidget>
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) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
size: const Size(double.infinity, 80),
painter: _TickerPainter(_controller.value),
);
},
);
}
}
class _TickerPainter extends CustomPainter {
final double progress;
_TickerPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.teal
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final x = size.width * progress;
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
}
@override
bool shouldRepaint(_TickerPainter old) => old.progress != progress;
}
متى تستخدم RepaintBoundary
RepaintBoundary أداة تحسين دقيقة وليست غلافاً افتراضياً. استخدمها في الحالات التالية:
- ودجت يتحرك باستمرار (مثل مؤشرات التقدم، المخططات الحية، تأثيرات الجسيمات)
- ودجت يستخدم
CustomPainterيُعيد رسم نفسه بتكرار عالٍ بناءً على تدفقات أو مؤقتات - شجرة فرعية كبيرة ومعقدة ومكلفة من ناحية الرسترة لكنها نادراً ما تتغير (مثل جدول بيانات ثقيل يتحدث كل 30 ثانية)
- عندما تتحقق عبر DevTools من وقوع إعادات رسم غير مرغوب فيها فعلاً
MaterialApp وانتقالات مسارات Navigator ومناطق عرض Scrollable. تحتاج إلى إضافته يدوياً فقط لنقاط الاستنزاف المخصصة التي تحددها عبر التوصيف.مقايضة التكلفة والفائدة
RepaintBoundary ليست مجانية. كل حد يُخصِّص نسيجاً منفصلاً لوحدة معالجة الرسوميات (يُسمى OffscreenLayer). الإفراط في استخدامها يمكن أن يُدهور الأداء من خلال:
- زيادة استهلاك ذاكرة وحدة معالجة الرسوميات (كل طبقة تحتفظ بصورة نقطية مُرسترة)
- إضافة حِمل على المُركِّب لمزج طبقات صغيرة كثيرة في كل إطار
- إمكانية تشغيل تمريرات رسم خارج الشاشة أكثر تكلفة
قائمة تحقق القرار
قبل إضافة RepaintBoundary، اسأل نفسك:
- هل يُعيد هذا الودجت رسم نفسه بتكرار مرتفع غير متوقع؟ (DevTools → تبويب الأداء)
- هل للودجت المُعيد رسمه ودجات شقيقة مكلفة يتم إبطالها؟
- هل تكلفة ذاكرة وحدة معالجة الرسوميات لنسيج إضافي تستحق توفير إعادة الرسم؟
- هل يمكن حل المشكلة بدلاً من ذلك بجعل
shouldRepaint()تُعيدfalseفي حالات أكثر؟
الخلاصة
RepaintBoundary أداة جراحية دقيقة في خط أنابيب رسم Flutter. تُنشئ طبقة تركيب مستقلة تمنع الشجرة الفرعية المُعيدة للرسم من إبطال جيرانها، مما يُقلل العمل الزائد على وحدة معالجة الرسوميات. استخدمها بتعمد: أولاً التوصيف باستخدام debugRepaintRainbowEnabled أو DevTools، ثم تحديد نقاط الاستنزاف الحقيقية، ثم تغليف تلك الودجات فقط. الإفراط يُضخِّم ذاكرة وحدة معالجة الرسوميات ويُضيف حِملاً على المُركِّب، مما قد يُفاقم الأمور. أفضل RepaintBoundary هي التي تُوضع عند حدود الطبقة الصحيحة بالضبط—لا أوسع ولا أضيق مما يلزم.