عند انشاء تطبيق لاحجام شاشات مختلفة تحتاج الى واجهة مستخدم متجاوبة مع جميع هذه الاحجام للجوالات والتابلت او الايباد وايضا تدعم الويب ** لان Flutter الان يدعم الويب **. يوجد العديد من الباكجات تقدم هذه الميزة ولكن هنا سنتعلم كيفية عملها او بناء التصميم المتجاوب الخاص بنا.
التصميم المتجاوب:
ببساطة هو دعم جميع احجام الشاشات باستخدام شفرة برمجية موحدة يعني انك لا تحتاج الى تخصيص شفرة برمجية لكل حجم او وضع للشاشة. وبما انك مبرمج Flutter فحتما تتاج الى دعم العديد من احجام الشاشات بحكم ان Flutter تعمل على الهواتف والتلفزيونات وايضا على الويب بالاضافة الى دعمها لبرامج سطح المكتب. يساعدك تعلم كيفية دعم الشاشات لتحديث تطبيقك ببساطة دون الحاجة لتتبع الشفرات البرمجية لكل شاشة.
هذا موقع انشئته باستخدام Flutter سابقاً يدعم الجوالات والايباد وايضا الويب. لايوجد تعقيد في التصميم الخاص بالموقع.
الان سنقوم بالبدء في التعرف على كيفية معرفة الطول والعرض للشاشة حتى نقوم بتمييز الشاشات الكبيرة من الصغيرة ونخصص لكل منها الشاشات الخاصه بها.
استخدام MediaQuery.of :
احد الخيارات لدعم جميع الشاشات هواستخدام دالة MediaQuery داخل دالة Build قي الشاشة. تعيد لك هذه الدالة الطول والعرض وهل الجهاز بشكل طولي او عرضي والعديد من المعلومات. في المثال التالي سنقوم باستخدام MediQuery لتحديد الطول والعرض وايضاً هل الجهاز بشكل افقي او عرضي. نريد في التطبيق اذا كان في وضع الطول ان نعرض BottomNavigationBar
اما في وضع العرض سنستخدم NavigationRail
.
الوضع العموي :
في المثال التالي سنقوم بتحديد اذا كان الجهاز بشكل افقي ام عمودي.
final isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape;
if (isLandscape) {
return Scaffold(
appBar: AppBar(leading: ProfileImage()),
body: Row(
children: <Widget>[
NavigationRail(
onDestinationSelected: widget.onSelectChange,
destinations: widget.menuItems
.map((menu) => NavigationRailDestination(
icon: Icon(menu.icon), label: Text(menu.title)))
.toList(),
selectedIndex: widget.currentIndex),
VerticalDivider(
width: 1,
color: Colors.grey.shade400,
),
Expanded(child: widget.body),
],
),
);
}
كيف عمل الشفرة البرمجية :
- في البداية قمنا بتحديد اذا كان الجهاز في الوضع العرض.
- اذا كان الجهاز في وضع العرض سنقوم بتغيير NavigationWidget الى
NavigationRail
. - الفائدة من دعم جميع الشاشات هو ان يتم استخدام البيانات مره واحدة وهذا ماحدث في
widget.body
.
الوضع الافقي:
تبقى حالة واحدة فقط وهي اذا كان الجهاز في وضع افقي سنقوم باعادة BottomNavigationBar
مع نفس العناصر ،ايضاً نفس البيانات.
return Scaffold(
appBar: AppBar(leading: ProfileImage()),
body: widget.body,
bottomNavigationBar: BottomNavigationBar(
currentIndex: widget.currentIndex,
unselectedItemColor: Colors.black87,
onTap: widget.onSelectChange,
selectedItemColor: Colors.blueAccent,
items: widget.menuItems
.map((e) => BottomNavigationBarItem(
icon: Icon(e.icon), title: Text(e.title)))
.toList()),
);
الان اصبح لديك تحكم ومعرفة باستخدام MediaQuery
لتحديد Orientations
للجهاز. طيب كيف الان نعرف هل الجهاز جوال او ايباد او موقع الكتروني باختصار كيف نعرف ابعاد الشاشة وعلى اساسها نقوم بتحديد التصميم لها. ?؟ عشان نجاوب على هذا السؤال راح نعرض كيف Layout
في Material Design لكل الأجهزة ونكتبه باستخدام Flutter كـWidget تستخدمه في كل مشاريعك.
قبل البداية في كتابة الشفرة البرمجية لازم نفصل كل شاشة لوحدها ونحللها ونشوف وش Widgets المخصص لكل شاشة والمميزات والي راح يكون مخفي او ظاهر من البداية.
راح نكتب الشفرة البرمجية وبتلاحظ تكرار بعضها ولكن هذا للتوضيح نهاية الدرس نلقى الشفرة البرمجية خالية من التكرار وقابلة للتطوير ??????.
تحليل الواجهة لسطح المكتب:
سنبدأ بالعمل على Layout لشاشات الكبيرة موقع او تلفزيون بالاضافة لتطبيقات سطح المكتب، قبل البدء دائماً حلل الواجهة وقرر وش راح تستخدم Widgets وكيف راح تعمل مع بعض لجميع الاجهزة والمقاسات.
- من النظرة الاولى عبارة عن صف من قسمين
Row
. - الجانب الايسر تحتوي على
Drawer
. - ايضاً على
Float Action Button
. - اما شاشة المحتوي فهي تحتوي على
AppBar
وRow
للمحتوى. - الجانب الايمن عبارة عن
Widget
يتم تخصيصة من المستخدم لللفلترة.
انشئ Widget جديد :
سنقوم بانشاء Widget جديد حتى يكون هو Parent او root لكل الشاشات. راح يكون اسمه responsive_scaffoled
او اي اسم يناسب تطبيقك. طبعا يكون statefull
حتى نتعامل مع عمليات الاختيار للقوائم الخ.
الان سنبدأ بتخصيص متى يعرض هذا التصميم وهو على الشاشات الكبيرة فنحتاج الان لتحديد مقاس الشاشة وبناء عليه نحدد التصميم المراد عرضه للمستخدم. يتم الحصول على ابعاد الشاشة باستخدام MediaQuery
ونحتاج هنا فقط عرض الشاشة لان التصميم يعتمد على العرض فقط.
MediaQuery.of(context).size.width // 920 ex
راح ننشئ extension
حتى تكون الشفرة البرمجية مرتبة وقابلة للقراءة. في الشفرة البرمجية التالية حددنا جميع مانحتاج الجهاز بالطول او العرض! هل الشاشة كبيرة ؟ ام متوسطة!.
extension ScreenSize on BuildContext {
MediaQueryData get mq => MediaQuery.of(this);
bool get isLandscape => mq.orientation == Orientation.landscape;
bool get isMediumScreen => mq.size.width > 640.0;
bool get isLargeScreen => mq.size.width > 960.0;
}
الان نبدأ كتابة الشفرة البرمجية لشاشات الكبيرة ومنها نتعلم كيف نستخدم extension method
وكيف توضح الشفرة وتخليها نظيفها، الشفرة البرمجية التالية توضح التصميم:
if (context.isLargeScreen) {
return Stack(
children: [
Row(
children: <Widget>[
Drawer(
child: Column(
children: menuItems
.map((e) => ListTile(
selected: widget.menuItems.indexOf(e) ==
widget.currentIndex,
onTap: () => _menuItemTapped(e),
title: Text(e.title),
leading: Icon(e.icon),
))
.toList(),
)),
VerticalDivider(
width: 1,
thickness: 10,
color: Colors.grey.shade300,
),
Expanded(
child: Scaffold(
appBar: AppBar(
title: widget.title,
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
)
],
),
body: Row(
children: <Widget>[
Expanded(child: widget.body),
widget.right
],
),
),
)
],
),
Positioned(
left: 275,
top: 100,
child: FloatingActionButton(
child: Icon(Icons.title),
onPressed: () {},
))
],
);
}
Stack
: استخدمناه حتى نحدد المكان للـFloat Action Button
.Row
: صف لتقسم الشاشة للـDrawer
وBody
.Scaffold
: للـAppBar
و ايضاً لتقسمBody
.- .. الخ.
التحليل تم تحويلة الى شفرة برمجية والنتيجة مشابهه للـمطلوب بشكل كبير كـLayout
.
الان اصبحت صفحة الويب جاهزة للاستخدام سننتقل الى بناء Layout للشاشات متوسطة الحجم مثل iPad او Tablet وستقوم باجراء تعديلات بسيطة ولكن قبل كل هذا نحتاج الى تحليل صفحة الشاشات المتوسطة لمعرفة التغيير الذي نحتاجة.
الشاشات متوسطة الحجم:
في الشاشات متوسطة الحجم غالبا مايكون Drawer
مخفي ويتم فتحه باستخدام زر في AppBar
وبما ان لدينا Drawer
جاهز من شاشة سطح المكتب سنقوم فقط بتعديل بسيط جدا وهو اضافة زر لفتح القائمة واخفائها.
if (context.isMediumScreen) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {},
),
floatingActionButtonLocation: TabletFabLocation(),
key: _scaffoldKey,
appBar: AppBar(
title: widget.title,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
}),
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
)
],
),
drawer: drawer,
body: Row(
children: <Widget>[Expanded(child: widget.body), widget.right],
),
);
}
يوجد بعض الاختلافات عن الشفرة البرمجية الخاصة بالشاشات الكبيرة وهو اننا قمنا بالاستغناء عن Stack
وايضاً انشئنا موقع مخصص للـFloat Action Button
حتى يكون في هذا الموقع. الان تبقى فقط الشفرة البرمجية للشاشات الصغيرة.
الشاشات الصغيرة:
الان وصلنا للمرحلة الاخيرة وهي دعم الشاشات الصغيرة وسنقوم بتحليل الاختلافات والاضافات الموجودة في تصميم الشاشات الصغيرة
نلاحظ من الصورة ان هناك Drawers
مخفية يمين ويسار ايضا هناك زر اضافي في AppBar
بالاضافة ان موقع Float Action Button
تغير. طبعا راح يكون التعديل سهل جدا لان Scaffold
يدعم Drawer
في الجهتين يمين ويسار بالاضافة الان ان Float Action Button
يكون في مكانة الافتراضي.
Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {},
),
key: _scaffoldKey,
appBar: AppBar(
title: widget.title,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
}),
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.tune),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
),
drawer: drawer,
endDrawer: widget.right,
body: widget.body,
);
الان اصبح لديك Widget يدعم جميع الشاشات الكبيرة والمتوسطة والصغيرة بامكانك تعديل الـWidgets بما يتناسب مع تصميمك اذا فهمت الالية لعمل التصميم. اذا كانت عناصر القائمة قليلة يمكنك الاستغناء عن Drawer
واستخدام ملاً Bottom Navigation
لتصميم افضل.
الكود كامل.
class ResponsiveScaffold extends StatefulWidget {
final Widget title;
final FloatingActionButton floatingActionButton;
final Widget body;
final int currentIndex;
final List<MenuItem> menuItems;
final ValueChanged<int> onItemSelected;
final Widget right;
const ResponsiveScaffold(
{Key key,
this.title,
this.floatingActionButton,
this.body,
this.currentIndex,
this.menuItems,
this.onItemSelected,
this.right})
: super(key: key);
@override
_ResponsiveScaffoldState createState() => _ResponsiveScaffoldState();
}
class _ResponsiveScaffoldState extends State<ResponsiveScaffold> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
final drawer = Drawer(
child: Column(
children: menuItems
.map((e) => ListTile(
selected: widget.menuItems.indexOf(e) == widget.currentIndex,
onTap: () => _menuItemTapped(e),
title: Text(e.title),
leading: Icon(e.icon),
))
.toList(),
));
if (context.isLargeScreen) {
return Stack(
children: [
Row(
children: <Widget>[
drawer,
VerticalDivider(
width: 1,
thickness: 10,
color: Colors.grey.shade300,
),
Expanded(
child: Scaffold(
appBar: AppBar(
title: widget.title,
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
)
],
),
body: Row(
children: <Widget>[
Expanded(child: widget.body),
widget.right
],
),
),
)
],
),
Positioned(left: 275, top: 100, child: widget.floatingActionButton)
],
);
}
if (context.isMediumScreen) {
return Scaffold(
floatingActionButton: widget.floatingActionButton,
floatingActionButtonLocation: TabletFabLocation(),
key: _scaffoldKey,
appBar: AppBar(
title: widget.title,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
}),
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
)
],
),
drawer: drawer,
body: Row(
children: <Widget>[Expanded(child: widget.body), widget.right],
),
);
}
return Scaffold(
floatingActionButton: widget.floatingActionButton,
key: _scaffoldKey,
appBar: AppBar(
title: widget.title,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
}),
backgroundColor: Colors.white,
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.tune),
onPressed: () {
_scaffoldKey.currentState.openEndDrawer();
},
),
],
),
drawer: drawer,
endDrawer: widget.right,
body: widget.body,
);
}
void _menuItemTapped(MenuItem menuItem) {
var idx = widget.menuItems.indexOf(menuItem);
if (idx != widget.currentIndex) {
widget.onItemSelected(idx);
}
}
}
extension ScreenSize on BuildContext {
MediaQueryData get mq => MediaQuery.of(this);
bool get isLandscape => mq.orientation == Orientation.landscape;
bool get isMediumScreen => mq.size.width > 640.0;
bool get isLargeScreen => mq.size.width > 960.0;
}