OrientationBuilder وتخطيطات المنصات المتعددة
اكتشاف اتجاه الجهاز
تحتاج العديد من التطبيقات إلى تكييف تخطيطها عندما يدير المستخدم جهازه. يوفر Flutter عنصر OrientationBuilder الذي يعيد بناء عنصره الفرعي كلما تغير اتجاه الجهاز بين الوضع العمودي والأفقي.
عنصر OrientationBuilder
يوفر OrientationBuilder قيمة Orientation (إما Orientation.portrait أو Orientation.landscape) لدالة البناء الخاصة به. يتيح لك هذا إرجاع عناصر مختلفة تمامًا بناءً على الاتجاه.
الاستخدام الأساسي لـ OrientationBuilder
class OrientationAwarePage extends StatelessWidget {
const OrientationAwarePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('عرض الاتجاه')),
body: OrientationBuilder(
builder: (context, orientation) {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: orientation == Orientation.portrait
? 2
: 4,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: 20,
itemBuilder: (context, index) {
return Card(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'عنصر \${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
);
},
),
);
}
}
OrientationBuilder الاتجاه بناءً على العرض والارتفاع المتاحين للعنصر الأب، وليس دوران الجهاز الفعلي. إذا تجاوز عرض الأب ارتفاعه، يكون الاتجاه landscape. هذا يعني أنه يعمل أيضًا داخل الحاويات المقيدة.
استخدام MediaQuery للاتجاه
يمكنك أيضًا التحقق من الاتجاه مباشرة من MediaQuery، وهو مفيد عندما تحتاج المعلومات خارج عنصر البناء.
فحص الاتجاه عبر MediaQuery
class MediaQueryOrientationPage extends StatelessWidget {
const MediaQueryOrientationPage({super.key});
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.orientationOf(context);
final size = MediaQuery.sizeOf(context);
final isLandscape = orientation == Orientation.landscape;
return Scaffold(
appBar: AppBar(
title: Text(
isLandscape ? 'الوضع الأفقي' : 'الوضع العمودي',
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('الشاشة: \${size.width.toInt()} x \${size.height.toInt()}'),
Text('الاتجاه: \${orientation.name}'),
const SizedBox(height: 16),
Expanded(
child: isLandscape
? _buildLandscapeLayout()
: _buildPortraitLayout(),
),
],
),
),
);
}
Widget _buildPortraitLayout() {
return ListView(
children: List.generate(
10,
(i) => Card(
child: ListTile(
leading: CircleAvatar(child: Text('\${i + 1}')),
title: Text('عنصر \${i + 1}'),
),
),
),
);
}
Widget _buildLandscapeLayout() {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 10,
itemBuilder: (context, i) => Card(
child: Center(
child: Text(
'عنصر \${i + 1}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
);
}
}
اكتشاف المنصة
عند بناء تطبيقات تعمل على منصات متعددة (iOS، Android، الويب، سطح المكتب)، غالبًا ما تحتاج إلى اكتشاف المنصة الحالية وتكييف واجهة المستخدم وفقًا لذلك.
فئة Platform
توفر مكتبة dart:io فئة Platform مع خصائص منطقية لكل نظام تشغيل. ومع ذلك، هذه الفئة غير متاحة على الويب.
اكتشاف المنصة (غير الويب)
import 'dart:io' show Platform;
class PlatformInfo {
static String get currentPlatform {
if (Platform.isAndroid) return 'Android';
if (Platform.isIOS) return 'iOS';
if (Platform.isMacOS) return 'macOS';
if (Platform.isWindows) return 'Windows';
if (Platform.isLinux) return 'Linux';
if (Platform.isFuchsia) return 'Fuchsia';
return 'غير معروف';
}
}
kIsWeb و defaultTargetPlatform
لاكتشاف المنصة الآمن للويب، يوفر Flutter kIsWeb من foundation.dart و defaultTargetPlatform من الإطار.
اكتشاف المنصة الآمن للويب
import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform;
class AdaptivePlatform {
static bool get isWeb => kIsWeb;
static bool get isMobile =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
static bool get isDesktop =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux);
static bool get isTablet {
// الأجهزة اللوحية هي أجهزة محمولة بشاشات أكبر
// نجمع فحص المنصة مع حجم الشاشة
return isMobile;
// يجب على المتصل أيضًا التحقق من عرض الشاشة > 600
}
}
dart:io في كود يعمل على الويب. سيسبب أخطاء ترجمة. استخدم دائمًا kIsWeb للتحقق من الويب أولاً، أو استخدم الاستيرادات الشرطية.
نظام التخطيط التكيفي
نهج قوي هو تحديد نقاط التوقف وإنشاء منشئ تخطيط تكيفي يختار تلقائيًا التخطيط المناسب للهاتف والجهاز اللوحي وسطح المكتب.
تخطيط تكيفي قائم على نقاط التوقف
enum DeviceType { phone, tablet, desktop }
class ResponsiveBreakpoints {
static const double tabletBreakpoint = 600;
static const double desktopBreakpoint = 1024;
static DeviceType getDeviceType(double width) {
if (width >= desktopBreakpoint) return DeviceType.desktop;
if (width >= tabletBreakpoint) return DeviceType.tablet;
return DeviceType.phone;
}
}
class AdaptiveLayout extends StatelessWidget {
final Widget Function(BuildContext context) phoneLayout;
final Widget Function(BuildContext context)? tabletLayout;
final Widget Function(BuildContext context)? desktopLayout;
const AdaptiveLayout({
super.key,
required this.phoneLayout,
this.tabletLayout,
this.desktopLayout,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return switch (deviceType) {
DeviceType.desktop =>
(desktopLayout ?? tabletLayout ?? phoneLayout)(context),
DeviceType.tablet =>
(tabletLayout ?? phoneLayout)(context),
DeviceType.phone =>
phoneLayout(context),
};
}
}
شبكة عمودية مقابل أفقية
إليك مثالًا عمليًا يجمع بين اكتشاف الاتجاه ونظام نقاط التوقف لإنشاء معرض صور يتكيف مع كل من الاتجاه وحجم الشاشة.
معرض صور تكيفي
class AdaptiveGallery extends StatelessWidget {
const AdaptiveGallery({super.key});
int _getColumnCount(BuildContext context, Orientation orientation) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return switch ((deviceType, orientation)) {
(DeviceType.phone, Orientation.portrait) => 2,
(DeviceType.phone, Orientation.landscape) => 4,
(DeviceType.tablet, Orientation.portrait) => 3,
(DeviceType.tablet, Orientation.landscape) => 5,
(DeviceType.desktop, _) => 6,
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('معرض الصور')),
body: OrientationBuilder(
builder: (context, orientation) {
final columns = _getColumnCount(context, orientation);
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 30,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Icon(
Icons.photo,
color: Colors.white.withValues(alpha: 0.7),
size: 32,
),
),
),
);
},
);
},
),
);
}
}
العرض المقسم للأجهزة اللوحية
تمتلك الأجهزة اللوحية مساحة شاشة كافية لعرض تخطيط رئيسي-تفصيلي. على الهواتف، تتنقل بين الشاشات؛ على الأجهزة اللوحية، تظهر كلتا اللوحتين جنبًا إلى جنب.
العرض المقسم رئيسي-تفصيلي
class SplitViewPage extends StatefulWidget {
const SplitViewPage({super.key});
@override
State<SplitViewPage> createState() => _SplitViewPageState();
}
class _SplitViewPageState extends State<SplitViewPage> {
int? _selectedIndex;
final List<Map<String, String>> _items = List.generate(
20,
(i) => {
'title': 'مقال \${i + 1}',
'body': 'هذا هو المحتوى الكامل للمقال \${i + 1}. '
'يحتوي على معلومات تفصيلية تُعرض '
'في لوحة التفاصيل على الشاشات الأكبر.',
},
);
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isWide = width >= 600;
return Scaffold(
appBar: AppBar(title: const Text('المقالات')),
body: isWide
? Row(
children: [
SizedBox(
width: 320,
child: _buildList(),
),
const VerticalDivider(width: 1),
Expanded(
child: _selectedIndex != null
? _buildDetail(_selectedIndex!)
: const Center(
child: Text('اختر مقالاً'),
),
),
],
)
: _buildList(),
);
}
Widget _buildList() {
return ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return ListTile(
selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.1),
title: Text(_items[index]['title']!),
subtitle: Text(
_items[index]['body']!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () {
setState(() => _selectedIndex = index);
final width = MediaQuery.sizeOf(context).width;
if (width < 600) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(
title: Text(_items[index]['title']!),
),
body: _buildDetail(index),
),
),
);
}
},
);
},
);
}
Widget _buildDetail(int index) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_items[index]['title']!,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
_items[index]['body']!,
style: const TextStyle(fontSize: 16),
),
],
),
);
}
}
التنقل التكيفي حسب المنصة
تمتلك المنصات المختلفة اصطلاحات تنقل مختلفة. تستخدم تطبيقات الهاتف المحمول أشرطة التنقل السفلية، بينما تستخدم تطبيقات سطح المكتب عادةً قضبان التنقل الجانبية أو الأدراج الكاملة.
غلاف التنقل التكيفي
class AdaptiveNavigationShell extends StatefulWidget {
const AdaptiveNavigationShell({super.key});
@override
State<AdaptiveNavigationShell> createState() =>
_AdaptiveNavigationShellState();
}
class _AdaptiveNavigationShellState
extends State<AdaptiveNavigationShell> {
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('الملف الشخصي'),
),
];
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return Scaffold(
body: Row(
children: [
if (deviceType == DeviceType.desktop)
NavigationRail(
extended: true,
selectedIndex: _selectedIndex,
destinations: _railDestinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
)
else if (deviceType == DeviceType.tablet)
NavigationRail(
extended: false,
selectedIndex: _selectedIndex,
destinations: _railDestinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
),
Expanded(
child: _buildPage(_selectedIndex),
),
],
),
bottomNavigationBar: deviceType == DeviceType.phone
? NavigationBar(
selectedIndex: _selectedIndex,
destinations: _destinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
)
: null,
);
}
Widget _buildPage(int index) {
return Center(
child: Text(
'صفحة \${index + 1}',
style: const TextStyle(fontSize: 24),
),
);
}
}
OrientationBuilder الاستجابة لتغييرات العمودي/الأفقي. اكتشاف المنصة مع kIsWeb و defaultTargetPlatform يمكّن فحوصات آمنة عبر المنصات. الجمع بين نقاط التوقف والاتجاه ينشئ تخطيطات تكيفية حقيقية. استخدم العروض المقسمة على الأجهزة اللوحية، والتنقل التكيفي لأحجام الشاشات المختلفة، واختبر دائمًا على عوامل شكل متعددة.