LayoutBuilder والتخطيطات التكيفية
ما هو LayoutBuilder؟
بينما يمنحك MediaQuery حجم الشاشة، يمنحك LayoutBuilder المساحة المتاحة من العنصر الأب. هذا فرق جوهري—العنصر داخل شريط جانبي لا يملك كامل عرض الشاشة المتاح. يتيح لك LayoutBuilder بناء عناصر متجاوبة تتكيف مع حجم حاويتها الفعلي، وليس حجم الجهاز.
MediaQuery.sizeOf(context) يُرجع أبعاد الشاشة. LayoutBuilder يوفر قيود العنصر الأب—المساحة الفعلية التي يجب أن يعمل عنصرك ضمنها. فضّل دائمًا LayoutBuilder عندما قد يُوضع عنصرك في حاويات بأحجام مختلفة.
عنصر LayoutBuilder
يوفر LayoutBuilder دالة بناء مع معاملين: BuildContext وكائن BoxConstraints يمثل القيود من العنصر الأب. تستخدم هذه القيود لتقرر أي تخطيط تعرضه.
استخدام LayoutBuilder الأساسي
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// constraints.maxWidth - أقصى عرض متاح
// constraints.maxHeight - أقصى ارتفاع متاح
// constraints.minWidth - أدنى عرض (عادةً 0)
// constraints.minHeight - أدنى ارتفاع (عادةً 0)
if (constraints.maxWidth > 600) {
return _buildWideLayout();
} else {
return _buildNarrowLayout();
}
},
)
BoxConstraints في LayoutBuilder
كائن BoxConstraints يخبرك بالضبط كم المساحة المتاحة. فهم خصائصه أمر ضروري:
فهم BoxConstraints
LayoutBuilder(
builder: (context, constraints) {
debugPrint('أدنى عرض: \${constraints.minWidth}');
debugPrint('أقصى عرض: \${constraints.maxWidth}');
debugPrint('أدنى ارتفاع: \${constraints.minHeight}');
debugPrint('أقصى ارتفاع: \${constraints.maxHeight}');
// فحوصات قيود مفيدة:
final isTight = constraints.isTight; // min == max
final isUnbounded = constraints.maxHeight == double.infinity;
final hasLooseWidth = constraints.minWidth == 0;
return Container(
width: constraints.maxWidth,
height: constraints.hasBoundedHeight
? constraints.maxHeight
: 400,
color: Colors.blue[50],
child: Center(
child: Text(
'المتاح: \${constraints.maxWidth.toStringAsFixed(0)} x '
'\${constraints.hasBoundedHeight ? constraints.maxHeight.toStringAsFixed(0) : "غير محدود"}',
),
),
);
},
)
أنماط نقاط التوقف
نمط شائع هو تعريف نقاط توقف تحدد أي تخطيط يُعرض. هذا النهج يحافظ على تنظيم الكود واتساقه عبر تطبيقك.
ثوابت نقاط التوقف
abstract class Breakpoints {
static const double mobile = 600;
static const double tablet = 900;
static const double desktop = 1200;
static const double wideDesktop = 1800;
}
// الاستخدام في LayoutBuilder
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < Breakpoints.mobile) {
return const MobileLayout();
} else if (constraints.maxWidth < Breakpoints.tablet) {
return const TabletLayout();
} else {
return const DesktopLayout();
}
},
)
فئة مساعد متجاوب
بناء مساعد متجاوب قابل لإعادة الاستخدام يُبسّط إنشاء التخطيطات التكيفية في أنحاء تطبيقك:
عنصر ResponsiveBuilder
class ResponsiveBuilder extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
const ResponsiveBuilder({
super.key,
required this.mobile,
this.tablet,
this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200 && desktop != null) {
return desktop!;
}
if (constraints.maxWidth >= 600 && tablet != null) {
return tablet!;
}
return mobile;
},
);
}
/// مساعد ثابت للحصول على نوع الجهاز الحالي
static ScreenType getScreenType(BoxConstraints constraints) {
if (constraints.maxWidth >= 1200) return ScreenType.desktop;
if (constraints.maxWidth >= 600) return ScreenType.tablet;
return ScreenType.mobile;
}
}
enum ScreenType { mobile, tablet, desktop }
// الاستخدام:
ResponsiveBuilder(
mobile: const MobileProductList(),
tablet: const TabletProductGrid(columns: 3),
desktop: const DesktopProductGrid(columns: 4),
)
LayoutBuilder بدلاً من MediaQuery في ResponsiveBuilder، يتكيف العنصر بشكل صحيح حتى عند وضعه داخل شريط جانبي أو حوار أو عرض مقسم—أماكن يكون فيها العرض المتاح أصغر بكثير من عرض الشاشة.
التنقل التكيفي: Rail مقابل Bottom
نمط تكيفي كلاسيكي هو التبديل بين BottomNavigationBar على الهاتف وNavigationRail على الشاشات الأكبر. LayoutBuilder يجعل هذا سهلاً:
التنقل التكيفي
class AdaptiveNavigation extends StatefulWidget {
const AdaptiveNavigation({super.key});
@override
State<AdaptiveNavigation> createState() => _AdaptiveNavigationState();
}
class _AdaptiveNavigationState extends State<AdaptiveNavigation> {
int _selectedIndex = 0;
static const List<NavigationDestination> _destinations = [
NavigationDestination(icon: Icon(Icons.home), label: 'الرئيسية'),
NavigationDestination(icon: Icon(Icons.search), label: 'البحث'),
NavigationDestination(icon: Icon(Icons.person), label: 'الملف الشخصي'),
];
static const List<NavigationRailDestination> _railDestinations = [
NavigationRailDestination(icon: Icon(Icons.home), label: Text('الرئيسية')),
NavigationRailDestination(icon: Icon(Icons.search), label: Text('البحث')),
NavigationRailDestination(icon: Icon(Icons.person), label: Text('الملف الشخصي')),
];
final List<Widget> _pages = const [
Center(child: Text('الصفحة الرئيسية')),
Center(child: Text('صفحة البحث')),
Center(child: Text('صفحة الملف الشخصي')),
];
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final useRail = constraints.maxWidth >= 600;
if (useRail) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
labelType: NavigationRailLabelType.all,
destinations: _railDestinations,
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(child: _pages[_selectedIndex]),
],
),
);
}
return Scaffold(
body: _pages[_selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
destinations: _destinations,
),
);
},
);
}
}
مثال عملي: تخطيط Master-Detail
على الشاشات العريضة، اعرض القائمة والتفاصيل جنبًا إلى جنب. على الشاشات الضيقة، تنقل بينهما:
نمط Master-Detail
class MasterDetailScreen extends StatefulWidget {
const MasterDetailScreen({super.key});
@override
State<MasterDetailScreen> createState() => _MasterDetailScreenState();
}
class _MasterDetailScreenState extends State<MasterDetailScreen> {
int? _selectedItemId;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 800;
if (isWide) {
// تخطيط جنبًا إلى جنب
return Row(
children: [
SizedBox(
width: 350,
child: _buildMasterList(),
),
const VerticalDivider(width: 1),
Expanded(
child: _selectedItemId != null
? _buildDetail(_selectedItemId!)
: const Center(
child: Text('اختر عنصرًا'),
),
),
],
);
}
// تخطيط متراص - استخدم Navigator للتفاصيل
return _buildMasterList();
},
);
}
Widget _buildMasterList() {
return ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
title: Text('عنصر \$index'),
subtitle: Text('وصف العنصر \$index'),
selected: _selectedItemId == index,
onTap: () {
setState(() => _selectedItemId = index);
// على الشاشات الضيقة، انتقل لصفحة التفاصيل
final isWide = MediaQuery.sizeOf(context).width >= 800;
if (!isWide) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: Text('عنصر \$index')),
body: _buildDetail(index),
),
),
);
}
},
);
},
);
}
Widget _buildDetail(int id) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.article, size: 80, color: Colors.blue[300]),
const SizedBox(height: 16),
Text('تفاصيل العنصر \$id',
style: const TextStyle(fontSize: 24)),
const SizedBox(height: 8),
Text('المحتوى الكامل للعنصر \$id يظهر هنا.'),
],
),
),
);
}
}
مثال عملي: شبكة متجاوبة
شبكة متجاوبة مع LayoutBuilder
class ResponsiveGrid extends StatelessWidget {
final List<Widget> children;
final double spacing;
const ResponsiveGrid({
super.key,
required this.children,
this.spacing = 16,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = _getColumnCount(constraints.maxWidth);
final itemWidth =
(constraints.maxWidth - spacing * (columns - 1)) / columns;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: children.map((child) {
return SizedBox(
width: itemWidth,
child: child,
);
}).toList(),
);
},
);
}
int _getColumnCount(double width) {
if (width >= 1200) return 4;
if (width >= 800) return 3;
if (width >= 500) return 2;
return 1;
}
}
مثال عملي: شريط جانبي تكيفي
تخطيط شريط جانبي تكيفي
class AdaptiveSidebar extends StatelessWidget {
final Widget sidebar;
final Widget body;
const AdaptiveSidebar({
super.key,
required this.sidebar,
required this.body,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
// عريض: شريط جانبي موسّع
return Row(
children: [
SizedBox(width: 280, child: sidebar),
const VerticalDivider(width: 1),
Expanded(child: body),
],
);
} else if (constraints.maxWidth >= 800) {
// متوسط: شريط جانبي مطوي (أيقونات فقط)
return Row(
children: [
SizedBox(width: 72, child: sidebar),
const VerticalDivider(width: 1),
Expanded(child: body),
],
);
} else {
// ضيق: استخدم Drawer
return Scaffold(
drawer: Drawer(child: sidebar),
body: body,
);
}
},
);
}
}
LayoutBuilder توفير قيود لا نهائية للبناء. إذا وضعت LayoutBuilder داخل عنصر قابل للتمرير بدون قيود محدودة، سيكون maxHeight هو double.infinity. تحقق دائمًا من constraints.hasBoundedHeight قبل استخدام maxHeight في الحسابات.
LayoutBuilder كلما احتجت لعناصر تتكيف مع مساحتها المتاحة بدلاً من حجم الشاشة. عرّف نقاط التوقف كثوابت للاتساق. ابنِ مساعدات متجاوبة قابلة لإعادة الاستخدام مثل ResponsiveBuilder للحفاظ على عدم تكرار الكود. ادمج LayoutBuilder مع أنماط التنقل التكيفي لإنشاء تطبيقات متجاوبة حقًا.