Flutter: نظام التخطيط
تخطيط Flutter كخط تجميع — الأب يمرر القيود، الابن يبلغ الحجم، الأب يحدد الموضع.
📋 المتطلبات السابقة: يجب أن تكون قد أكملت ما يلي أولًا
- الدرس 3: أساسيات الويدجت
1. ما ستتعلمه
- نموذج الصندوق: بروتوكول تمرير القيود: القيود ← الحجم ← الموضع
- التخطيط الخطي: Row/Column مع محاذاة Main/CrossAxisAlignment
- التخطيط المرن: Flex/Expanded/Flexible التوزيع النسبي
- التخطيط الطبقي: Stack/Positioned للتراكبات والشارات
- تخطيط شبكة منتجات ShopApp: GridView.count والتدفق المتساقط
2. قصة من مصمم واجهات انتقل إلى التطوير
(1) المشكلة: كابوس شرائط الفائض الصفراء-السوداء
تلقى بوب مواصفات تصميم واجهة ShopApp — بطاقات المنتجات تحتاج صورة على اليمين ومعلومات على اليسار، مع صف سعر في الأسفل. وضع Row داخل Column، فظهرت تحذيرات الفائض المخططة بالأصفر والأسود على الشاشة. بعد تعديل padding وmargin طويلًا، إما أن يُقطع النص أو تتشوه الصور. 1000 منتج، 10 أحجام شاشة، مشاكل تخطيط في كل مكان.
(2) حل تمرير القيود
لتخطيط Flutter قاعدة أساسية واحدة: القيود تمرر للأسفل، الأحجام تبلغ للأعلى، المواضع يحددها الأب. بفهم هذه القاعدة، يمكن لبوب توقع سلوك كل ويدجت.
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')),
],
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) النتيجة: تخطيطات صحيحة من المحاولة الأولى
بعد إتقان تمرير القيود، تحسنت دقة تخطيط بوب من المحاولة الأولى من 30% إلى 90%، واختفت تحذيرات الفائض تمامًا.
3. نموذج الصندوق وتمرير القيود
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
> الإخراج: شغّل محليًا باستخدام 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 داخل صفحة |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: عرض تمرير القيود
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)')),
),
),
],
),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
4. التخطيط الخطي: Row وColumn
(1) خصائص المحاذاة
| الخاصية | الاتجاه | تأثير Row | تأثير Column |
|---|---|---|---|
mainAxisAlignment |
المحور الرئيسي | محاذاة أفقية | محاذاة عمودية |
crossAxisAlignment |
المحور المتقاطع | محاذاة عمودية | محاذاة أفقية |
mainAxisSize |
حجم المحور الرئيسي | العرض | الارتفاع |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: صف معلومات المنتج (Row)
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],
)),
],
),
],
),
),
],
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(2) مقارنة قيم المحاذاة الشائعة
| MainAxisAlignment | التأثير |
|---|---|
start |
محاذاة للبداية (الافتراضي) |
center |
توسيط |
end |
محاذاة للنهاية |
spaceBetween |
الأول/الأخير على الحواف، مسافات متساوية بينهما |
spaceAround |
مسافات متساوية على جانبي كل عنصر |
spaceEvenly |
كل المسافات متساوية |
5. التخطيط المرن: Expanded وFlexible
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تخطيط مرن للسعر والزر
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'),
),
),
],
),
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| الويدجت | السلوك | المساحة المتبقية | حالة الاستخدام |
|---|---|---|---|
Expanded |
ملء إجباري | توزع نسبيًا | يجب ملء كل المساحة |
Flexible |
ملء اختياري | توزع نسبيًا، يمكن أن يكون أقل | مرن لكن غير إجباري |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: Expanded مقابل Flexible
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'))),
],
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. التخطيط الطبقي: Stack وPositioned
يسمح Stack للأبناء بالتراكب بترتيب الرسم، وPositioned يتحكم في الموضع المطلق.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: شارات صورة المنتج
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)),
),
),
],
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| خاصية Stack | الوصف | الافتراضي |
|---|---|---|
alignment |
محاذاة الأبناء غير الموضعين | AlignmentDirectional.topStart |
fit |
كيف يتكيف الأبناء | StackFit.loose |
clipBehavior |
قص الفائض | Clip.hardEdge |
7. تخطيط شبكة GridView
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: شبكة منتجات ShopApp
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,
);
},
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| خاصية GridView | الوصف |
|---|---|
crossAxisCount |
عدد الأعمدة في كل صف |
mainAxisSpacing |
تباعد المحور الرئيسي |
crossAxisSpacing |
تباعد المحور المتقاطع |
childAspectRatio |
نسبة عرض إلى ارتفاع العنصر الفرعي |
padding |
حشو داخلي |
8. مثال كامل: تخطيط الصفحة الرئيسية لـ ShopApp
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),
],
),
],
),
),
),
],
),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
📖 ملخص
- جوهر تخطيط Flutter: القيود تمرر للأسفل، الأحجام تبلغ للأعلى، المواضع يحددها الأب
- Row/Column تخطيطات خطية، تتحكم عبر Main/CrossAxisAlignment
- Expanded يملأ إجباريًا، Flexible يتكيف بشكل مرن، flex يتحكم في النسب
- Stack + Positioned يُنشئ تأثيرات طبقية (شارات، تراكبات)
- GridView.count يبني شبكات المنتجات، LayoutBuilder يتيح أعداد أعمدة متجاوبة
📝 تمارين
- أساسي (الصعوبة ⭐): أنشئ شريط وسوم فئات بتمرير أفقي (Chip + ListView اتجاه أفقي).
- متوسط (الصعوبة ⭐⭐): نفّذ تخطيط صفحة تفاصيل المنتج: صورة كبيرة في الأعلى + قسم معلومات في المنتصف + شريط سعر/زر شراء في الأسفل، باستخدام Column + Expanded.
- متقدم (الصعوبة ⭐⭐⭐): استخدم LayoutBuilder لتنفيذ شبكة منتجات متجاوبة: عمودان على الهاتف، 3 على الجهاز اللوحي، 4 على الحاسوب، مع التبديل في الوقت الفعلي عند تغيير حجم النافذة.