انشاء تطبيق متجاوب لجميع الشاشات Flutter

عند انشاء تطبيق لاحجام شاشات مختلفة تحتاج الى واجهة مستخدم متجاوبة مع جميع هذه الاحجام للجوالات والتابلت او الايباد وايضا تدعم الويب ** لان 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 تستخدمه في كل مشاريعك.

هذا اشكل Layout على كل احجام الاجهزة باستخدام Material Design

قبل البداية في كتابة الشفرة البرمجية لازم نفصل كل شاشة لوحدها ونحللها ونشوف وش 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 وستقوم باجراء تعديلات بسيطة ولكن قبل كل هذا نحتاج الى تحليل صفحة الشاشات المتوسطة لمعرفة التغيير الذي نحتاجة.

الشاشات متوسطة الحجم:

medum screen في الشاشات متوسطة الحجم غالبا مايكون 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],
        ),
      );
    }
اذا كان Drawer مغلق
اذا كان Drawer مفتوح

يوجد بعض الاختلافات عن الشفرة البرمجية الخاصة بالشاشات الكبيرة وهو اننا قمنا بالاستغناء عن Stack وايضاً انشئنا موقع مخصص للـFloat Action Button حتى يكون في هذا الموقع. الان تبقى فقط الشفرة البرمجية للشاشات الصغيرة.

الشاشات الصغيرة:

الان وصلنا للمرحلة الاخيرة وهي دعم الشاشات الصغيرة وسنقوم بتحليل الاختلافات والاضافات الموجودة في تصميم الشاشات الصغيرة mobile menu-min نلاحظ من الصورة ان هناك 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,
    );
Drawer يمين
المحتوى
Drawer اليسار

الان اصبح لديك 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;
}
المصادر

Responsive layout grid
Creating responsive apps

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *