Flutter: أساسيات الويدجت

الويدجت هي لبنات بناء Flutter — افهم الويدجت وقد فهمت حمض Flutter النووي.

📋 المتطلبات السابقة: يجب أن تكون قد أكملت ما يلي أولًا

1. ما ستتعلمه


2. قصة حقيقية من مطوّر واجهة أمامية انتقل إلى Flutter

(1) المشكلة: فخ عقلية DOM

انتقل بوب من React إلى Flutter وبغريزة حاول فهم الويدجت عبر نموذج ذهني "المكوّن يعرض DOM". وجد أن بطاقات المنتجات تُعاد بناؤها بشكل متكرر أثناء تمرير القائمة، مما تسبب في بطء صفحة منتجات من 1000 عنصر إلى 30 إطارًا في الثانية. والأكثر إرباكًا أن إضافة key للقائمة جعلت الأداء أسوأ — لأنه كان يستخدم UniqueKey.

(2) حل بنية الأشجار الثلاث

Flutter ليس مجرد تعيين بسيط "مكوّن ← DOM"، بل بنية ثلاثية الطبقات "ويدجت ← عنصر ← RenderObject". الويدجت هي أوصاف إعدادية (خفيفة)، العناصر تدير المثيلات (جوهر الفرق)، وRenderObjects تتولى التخطيط والرسم (ثقيلة). بفهم الأشجار الثلاث، أدرك بوب أنه يجب عليه التحسين على مستوى العنصر عبر الفرق، بدلًا من إعادة البناء على مستوى الويدجت.

DART
import 'package:flutter/material.dart';

// Lightweight widget: just configuration
const ProductCard({required this.product, super.key});

// Flutter internally creates Element and RenderObject
// Widget rebuilds are cheap - Element diffing avoids RenderObject rebuild
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) النتيجة: تمرير سلس عند 60 إطارًا في الثانية

بعد التحول إلى ValueKey(product.id)، طابق فرق العنصر العناصر بدقة، وحافظت قائمة المنتجات الـ 1000 على 60 إطارًا مستقرًا في الثانية.


3. الأشجار الثلاث: Widget-Element-RenderObject

100%
graph LR
    W[Widget Tree] --> E[Element Tree]
    E --> R[RenderObject Tree]
    W -.->|canUpdate| E
    E -.->|adoptChild| R
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(1) مسؤوليات الأشجار الثلاث

الطبقة المنشئ المسؤولية الوزن
الويدجت build() من المطور وصف إعدادات غير قابل للتغيير خفيف جدًا
العنصر إطار العمل تلقائيًا إدارة بنية الشجرة، فرق التحديثات متوسط
RenderObject إطار العمل تلقائيًا القياس، التخطيط، الرسم ثقيل

(2) قاعدة canUpdate

يحدد العنصر ما إذا كان سيعيد الاستخدام عبر Widget.canUpdate(oldWidget, newWidget):

DART
import 'package:flutter/material.dart';

// canUpdate logic
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
السيناريو runtimeType key النتيجة
نفس النوع، نفس المفتاح نفسه نفسه إعادة استخدام العنصر
نفس النوع، مفتاح مختلف نفسه مختلف إعادة بناء العنصر
نوع مختلف مختلف - إعادة بناء العنصر

4. الويدجت الأساسية الشائعة

(1) النص والأنماط

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: نص بأنماط مختلفة

DART
import 'package:flutter/material.dart';

// Basic text
const Text('ShopApp')

// Styled text
Text(
  'Flash Sale!',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.red,
    letterSpacing: 1.5,
  ),
)

// Rich text with multiple styles
Text.rich(
  TextSpan(
    text: 'Price: ',
    style: const TextStyle(fontSize: 16, color: Colors.grey),
    children: [
      TextSpan(
        text: '\$99.99',
        style: const TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
          color: Colors.green,
        ),
      ),
      TextSpan(
        text: ' \$149.99',
        style: TextStyle(
          fontSize: 14,
          decoration: TextDecoration.lineThrough,
          color: Colors.grey[400],
        ),
      ),
    ],
  ),
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(2) Container: الحاوية الشاملة

خاصية Container الغرض النوع
padding حشو داخلي EdgeInsets
margin هامش خارجي EdgeInsets
decoration زخرفة الخلفية BoxDecoration
constraints قيود BoxConstraints
alignment محاذاة الويدجت الفرعية Alignment

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: بطاقة منتج Container

DART
import 'package:flutter/material.dart';

Container(
  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  padding: const EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 8,
        offset: const Offset(0, 2),
      ),
    ],
  ),
  child: Row(
    children: [
      ClipRRect(
        borderRadius: BorderRadius.circular(8),
        child: Image.network(
          'https://cdn.shopapp.com/products/laptop.jpg',
          width: 80,
          height: 80,
          fit: BoxFit.cover,
        ),
      ),
      const SizedBox(width: 12),
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('Pro Laptop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
            const SizedBox(height: 4),
            Text('\$1,299.99', style: const TextStyle(fontSize: 18, color: Colors.green)),
          ],
        ),
      ),
    ],
  ),
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) الأيقونة وIconButton

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: الأيقونات في سياق التجارة الإلكترونية

DART
import 'package:flutter/material.dart';

// Icon with color and size
const Icon(Icons.shopping_cart, color: Colors.blue, size: 32)

// IconButton for actions
IconButton(
  icon: const Icon(Icons.favorite_border),
  onPressed: () {
    // Add to wishlist
  },
  tooltip: 'Add to Wishlist',
)

// Badge icon for cart count
Stack(
  children: [
    const Icon(Icons.shopping_cart, size: 28),
    Positioned(
      right: 0,
      top: 0,
      child: Container(
        padding: const EdgeInsets.all(2),
        decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
        constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
        child: const Text('3', style: TextStyle(fontSize: 10, color: Colors.white),
          textAlign: TextAlign.center),
      ),
    ),
  ],
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

5. آلية Key

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

(1) أنواع Key

نوع Key أساس المقارنة حالة الاستخدام
ValueKey(value) تساوي القيم عناصر القائمة بمعرفات فريدة (مثل product.id)
ObjectKey(object) مرجع الكائن مثيلات كائنات فريدة
UniqueKey() مختلف في كل مرة فرض عدم إعادة الاستخدام (نادرًا ما يُستخدم)
GlobalKey فريد عالميًا الوصول إلى State عبر الويدجت

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: دور Key في القوائم

DART
import 'package:flutter/material.dart';

// Simplified class definition
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}

// BAD: No key, Flutter matches by position
ListView(
  children: [
    ProductTile(product: products[0]), // index 0
    ProductTile(product: products[1]), // index 1
  ],
)

// When products reorder, Element mismatches State
// Solution: Use ValueKey with unique id

// GOOD: ValueKey enables correct Element-Widget matching
ListView(
  children: [
    ProductTile(key: ValueKey(products[0].id), product: products[0]),
    ProductTile(key: ValueKey(products[1].id), product: products[1]),
  ],
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. مثال كامل: ويدجت بطاقة منتج ShopApp

DART
import 'package:flutter/material.dart';

class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final double rating;
  final int reviewCount;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
    this.rating = 0.0,
    this.reviewCount = 0,
  });
}

class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback? onAddToCart;
  final VoidCallback? onTap;

  const ProductCard({
    super.key,
    required this.product,
    this.onAddToCart,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        clipBehavior: Clip.antiAlias,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        elevation: 2,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Expanded(
              flex: 3,
              child: Stack(
                children: [
                  Image.network(product.imageUrl, fit: BoxFit.cover,
                    width: double.infinity),
                  if (product.price < 50)
                    Positioned(top: 8, left: 8,
                      child: _buildBadge('SALE', Colors.red)),
                ],
              ),
            ),
            Expanded(
              flex: 2,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(product.name, maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(fontWeight: FontWeight.w600)),
                    const SizedBox(height: 4),
                    Text('\$${product.price.toStringAsFixed(2)}',
                      style: const TextStyle(color: Colors.green, fontSize: 16)),
                    const Spacer(),
                    Align(
                      alignment: Alignment.centerRight,
                      child: IconButton(
                        icon: const Icon(Icons.add_shopping_cart, size: 20),
                        onPressed: onAddToCart,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBadge(String text, Color color) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
      decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
      child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 10)),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س متى تُستدعى طريقة build في StatelessWidget؟
ج عند إعادة بناء ويدجت أب، أو عندما يُحرّك setState إعادة بناء أب. لا يمكن لـ StatelessWidget نفسه تحريك إعادة بناء.
س لماذا صُممت الويدجت لتكون غير قابلة للتغيير؟
ج عدم القابلية للتغيير يعني أنه يمكن إنشاؤها بشكل متكرر دون القلق من الآثار الجانبية. يمكن لإطار Flutter مقارنة الويدجت القديمة والجديدة بأمان لتحديث ما إذا كان العنصر سيُحدَّث.
س متى أحتاج لاستخدام Key؟
ج عندما تتشارك الويدجت في القائمة نفس النوع لكن ببيانات مختلفة، وقد تُضاف أو تُزال أو تُعاد ترتيب العناصر. القوائم الثابتة عادة لا تحتاج مفاتيح.
س GlobalKey يسمح بالوصول إلى State عبر الويدجت — ألا يكسر التغليف؟
ج نعم، GlobalKey هو "فتحة هروب" ويجب استخدامه باعتدال. حالات الاستخدام الشائعة: التحقق من النماذج، إدارة ScrollController. يُفضّل تمرير البيانات عبر الاستدعاءات الرجعية.
س ما الفرق بين Container وScaffold؟
ج Container حاوية زخرفية عامة؛ Scaffold هيكل صفحة Material Design يوفر فتحات لـ AppBar/Body/FAB/Drawer/BottomNav.
س ماذا لو فشل تحميل Image.network؟
ج استخدم المعامل errorBuilder لعرض صورة بديلة، مع حزمة cached_network_image للتخزين المؤقت ومؤشرات التحميل.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): أنشئ ويدجت UserAvatar من StatelessWidget يقبل الاسم وimageUrl، ويعرض صورة دائرية واسم المستخدم.
  2. متوسط (الصعوبة ⭐⭐): نفّذ ويدجت PriceTag يدعم عرض السعر الأصلي/المخفض، مع السعر المخفض بنص أخضر كبير والسعر الأصلي بخط مشطوب.
  3. متقدم (الصعوبة ⭐⭐⭐): ابنِ قائمة منتجات قابلة للفرز حيث يعتمد إعادة الترتيب بالسحب على ValueKey لضمان تبعية State بشكل صحيح، وبعد الفرز اعرض الترتيب الجديد عبر SnackBar.

← السابق | التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%