Flutter: نظام التخطيط

تخطيط Flutter كخط تجميع — الأب يمرر القيود، الابن يبلغ الحجم، الأب يحدد الموضع.

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

1. ما ستتعلمه


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

(1) المشكلة: كابوس شرائط الفائض الصفراء-السوداء

تلقى بوب مواصفات تصميم واجهة ShopApp — بطاقات المنتجات تحتاج صورة على اليمين ومعلومات على اليسار، مع صف سعر في الأسفل. وضع Row داخل Column، فظهرت تحذيرات الفائض المخططة بالأصفر والأسود على الشاشة. بعد تعديل padding وmargin طويلًا، إما أن يُقطع النص أو تتشوه الصور. 1000 منتج، 10 أحجام شاشة، مشاكل تخطيط في كل مكان.

(2) حل تمرير القيود

لتخطيط Flutter قاعدة أساسية واحدة: القيود تمرر للأسفل، الأحجام تبلغ للأعلى، المواضع يحددها الأب. بفهم هذه القاعدة، يمكن لبوب توقع سلوك كل ويدجت.

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

// Parent gives tight constraints: exactly 200x100
SizedBox(
  width: 200,
  height: 100,
  child: Text('I will be 200x100'), // Text forced to 200x100
)

// Parent gives loose constraints: 0 to maxWidth
Row(
  children: [
    Expanded(child: Text('I take remaining space')),
    SizedBox(width: 80, child: Text('I am 80 wide')),
  ],
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) النتيجة: تخطيطات صحيحة من المحاولة الأولى

بعد إتقان تمرير القيود، تحسنت دقة تخطيط بوب من المحاولة الأولى من 30% إلى 90%، واختفت تحذيرات الفائض تمامًا.


3. نموذج الصندوق وتمرير القيود

100%
graph TD
    subgraph Constraint Passing
        P[Parent Constraints] --> C[Child Layout]
        C --> S[Child Size]
        S --> P
    end
    subgraph Common Layouts
        R[Row] --> EX1[Expanded]
        CO[Column] --> EX2[Flexible]
        ST[Stack] --> POS[Positioned]
    end
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(1) أنواع القيود

النوع الوصف مثال
محكم min = max، حجم ثابت SizedBox(width: 100)
فضفاض min = 0، max = max الأب الابن داخل Center
غير محدود max = ما لا نهاية Column داخل ListView
محدود كل من min وmax محدودان Column داخل صفحة

▶ مثال

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

: عرض تمرير القيود

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

// Demo: constraint passing in action
class ConstraintDemo extends StatelessWidget {
  const ConstraintDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Tight constraint: exactly 200 height
          Container(
            height: 200,
            color: Colors.blue[100],
            child: const Center(child: Text('Height: 200 (tight)')),
          ),
          // Loose constraint: takes remaining space
          Expanded(
            child: Container(
              color: Colors.green[100],
              child: const Center(child: Text('Expanded (loose, fills remaining)')),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

4. التخطيط الخطي: Row وColumn

(1) خصائص المحاذاة

الخاصية الاتجاه تأثير Row تأثير Column
mainAxisAlignment المحور الرئيسي محاذاة أفقية محاذاة عمودية
crossAxisAlignment المحور المتقاطع محاذاة عمودية محاذاة أفقية
mainAxisSize حجم المحور الرئيسي العرض الارتفاع

▶ مثال

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

: صف معلومات المنتج (Row)

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

// Product info row: image + details
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Fixed-size image
    ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Image.network(
        'https://cdn.shopapp.com/laptop.jpg',
        width: 100,
        height: 100,
        fit: BoxFit.cover,
      ),
    ),
    const SizedBox(width: 12),
    // Flexible details
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Pro Laptop 16"',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          const SizedBox(height: 4),
          Text('Electronics > Laptops',
            style: TextStyle(fontSize: 12, color: Colors.grey[600])),
          const SizedBox(height: 8),
          Row(
            children: [
              const Text('\$1,299.99',
                style: TextStyle(fontSize: 18, color: Colors.green)),
              const SizedBox(width: 8),
              Text('\$1,599.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) مقارنة قيم المحاذاة الشائعة

MainAxisAlignment التأثير
start محاذاة للبداية (الافتراضي)
center توسيط
end محاذاة للنهاية
spaceBetween الأول/الأخير على الحواف، مسافات متساوية بينهما
spaceAround مسافات متساوية على جانبي كل عنصر
spaceEvenly كل المسافات متساوية

5. التخطيط المرن: Expanded وFlexible

▶ مثال

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

: تخطيط مرن للسعر والزر

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

// Bottom bar: price (expanded) + button (fixed)
Container(
  padding: const EdgeInsets.all(12),
  child: Row(
    children: [
      Expanded(
        flex: 2, // Takes 2/3 of space
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Total', style: TextStyle(fontSize: 12)),
            Text('\$${total.toStringAsFixed(2)}',
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          ],
        ),
      ),
      Expanded(
        flex: 1, // Takes 1/3 of space
        child: ElevatedButton(
          onPressed: onCheckout,
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 16)),
          child: const Text('Checkout'),
        ),
      ),
    ],
  ),
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
الويدجت السلوك المساحة المتبقية حالة الاستخدام
Expanded ملء إجباري توزع نسبيًا يجب ملء كل المساحة
Flexible ملء اختياري توزع نسبيًا، يمكن أن يكون أقل مرن لكن غير إجباري

▶ مثال

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

: Expanded مقابل Flexible

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

Row(
  children: [
    // Expanded: must fill allocated space
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, child: const Text('Expanded')),
    ),
    // Flexible: can be smaller than allocated space
    Flexible(
      flex: 1,
      child: Container(
        color: Colors.blue,
        child: const Text('Flexible'), // May not fill all space
      ),
    ),
    // Fixed width
    SizedBox(width: 80, child: Container(color: Colors.green, child: const Text('Fixed'))),
  ],
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. التخطيط الطبقي: Stack وPositioned

يسمح Stack للأبناء بالتراكب بترتيب الرسم، وPositioned يتحكم في الموضع المطلق.

▶ مثال

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

: شارات صورة المنتج

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

// Simplified class definition
class Product {
  final String imageUrl;
  const Product({required this.imageUrl});
}

// Product image with discount badge and favorite button
Stack(
  children: [
    // Base image
    ClipRRect(
      borderRadius: BorderRadius.circular(12),
      child: Image.network(
        product.imageUrl,
        height: 200,
        width: double.infinity,
        fit: BoxFit.cover,
      ),
    ),
    // Discount badge (top-left)
    Positioned(
      top: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(4),
        ),
        child: Text('${discountPercent}% OFF',
          style: const TextStyle(color: Colors.white, fontSize: 12)),
      ),
    ),
    // Favorite button (top-right)
    Positioned(
      top: 8,
      right: 8,
      child: CircleAvatar(
        backgroundColor: Colors.white.withOpacity(0.8),
        child: const Icon(Icons.favorite_border, size: 18),
      ),
    ),
    // Stock indicator (bottom)
    Positioned(
      bottom: 8,
      left: 8,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
        decoration: BoxDecoration(
          color: Colors.black54,
          borderRadius: BorderRadius.circular(4),
        ),
        child: const Text('In Stock', style: TextStyle(color: Colors.white, fontSize: 10)),
      ),
    ),
  ],
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
خاصية Stack الوصف الافتراضي
alignment محاذاة الأبناء غير الموضعين AlignmentDirectional.topStart
fit كيف يتكيف الأبناء StackFit.loose
clipBehavior قص الفائض Clip.hardEdge

7. تخطيط شبكة GridView

▶ مثال

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

: شبكة منتجات ShopApp

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});
}

// Two-column product grid
GridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  childAspectRatio: 0.75, // height = width / 0.75
  padding: const EdgeInsets.all(8),
  children: products.map((product) => ProductCard(
    key: ValueKey(product.id),
    product: product,
    onAddToCart: () => addToCart(product),
  )).toList(),
)

// Responsive grid with LayoutBuilder
LayoutBuilder(
  builder: (context, constraints) {
    int columns = (constraints.maxWidth / 180).floor().clamp(2, 4);
    return GridView.count(
      crossAxisCount: columns,
      mainAxisSpacing: 8,
      crossAxisSpacing: 8,
      childAspectRatio: 0.75,
      children: productWidgets,
    );
  },
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
خاصية GridView الوصف
crossAxisCount عدد الأعمدة في كل صف
mainAxisSpacing تباعد المحور الرئيسي
crossAxisSpacing تباعد المحور المتقاطع
childAspectRatio نسبة عرض إلى ارتفاع العنصر الفرعي
padding حشو داخلي

8. مثال كامل: تخطيط الصفحة الرئيسية لـ ShopApp

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ShopApp'),
        actions: [
          Stack(
            children: [
              IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
              Positioned(right: 4, top: 4,
                child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
                  child: const Text('5', style: TextStyle(fontSize: 10, color: Colors.white)))),
            ],
          ),
        ],
      ),
      body: CustomScrollView(
        slivers: [
          // Banner section
          SliverToBoxAdapter(
            child: Container(
              height: 180,
              color: Colors.blue[50],
              child: const Center(child: Text('Flash Sale - Up to 50% OFF',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
            ),
          ),
          // Category chips
          SliverToBoxAdapter(
            child: SizedBox(
              height: 50,
              child: ListView(
                scrollDirection: Axis.horizontal,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                children: ['All', 'Electronics', 'Clothing', 'Books', 'Home']
                  .map((c) => Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 4),
                    child: Chip(label: Text(c)),
                  )).toList(),
              ),
            ),
          ),
          // Product grid
          SliverPadding(
            padding: const EdgeInsets.all(8),
            sliver: SliverGrid.count(
              crossAxisCount: 2,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
              childAspectRatio: 0.7,
              children: List.generate(20, (i) => _buildProductCard(i)),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildProductCard(int index) {
    return Card(
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.grey[200],
              child: const Icon(Icons.image, size: 48, color: Colors.grey),
            ),
          ),
          Expanded(
            flex: 2,
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Product ${index + 1}', maxLines: 1, overflow: TextOverflow.ellipsis),
                  const Spacer(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('\$${(19.99 + index * 10).toStringAsFixed(2)}',
                        style: const TextStyle(color: Colors.green)),
                      const Icon(Icons.add_circle, size: 20),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س ماذا أفعل بشأن شرائط الفائض الصفراء-السوداء في Row؟
ج العرض الإجمالي لأبناء Row يتجاوز عرض Row. غلّف الأبناء متغيّري العرض بـ Expanded، أو استخدم Flexible للتحجيم التكيفي.
س ما الفرق بين Expanded وFlexible؟
ج Expanded يُجبر الابن على ملء المساحة المخصصة له (يكافئ Flexible(fit: FlexFit.tight))؛ Flexible يسمح للابن بأن يكون أصغر من المخصص (fit: FlexFit.loose).
س كيف يتم وضع الأبناء غير الموضعين في Stack؟
ج يوضعون وفقًا لخاصية alignment في Stack، الافتراضي هو الزاوية العلوية اليسرى.
س ما الفرق بين GridView.count وGridView.builder؟
ج count لعدد صغير وثابت من العناصر؛ builder يستخدم التحميل الكسول لقوائم البيانات الكبيرة (استخدم builder لقوائم المنتجات بمستوى المليون).
س لماذا يسبب ListView داخل Column خطأ؟
ج Column يعطي أبناءه قيود ارتفاع غير محدودة، وListView لا يمكنه تحديد ارتفاعه. غلّف ListView بـ Expanded، أو استخدم shrinkWrap: true.
س كيف تحسب childAspectRatio؟
ج النسبة = العرض / الارتفاع. إذا كان كل عنصر بعرض 180 وارتفاع 240، النسبة = 180/240 = 0.75.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): أنشئ شريط وسوم فئات بتمرير أفقي (Chip + ListView اتجاه أفقي).
  2. متوسط (الصعوبة ⭐⭐): نفّذ تخطيط صفحة تفاصيل المنتج: صورة كبيرة في الأعلى + قسم معلومات في المنتصف + شريط سعر/زر شراء في الأسفل، باستخدام Column + Expanded.
  3. متقدم (الصعوبة ⭐⭐⭐): استخدم LayoutBuilder لتنفيذ شبكة منتجات متجاوبة: عمودان على الهاتف، 3 على الجهاز اللوحي، 4 على الحاسوب، مع التبديل في الوقت الفعلي عند تغيير حجم النافذة.

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

Web-Tutorial.com

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

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

100%