تطبيقك الأول على Flutter

1. مقدمة

‫Flutter هي مجموعة أدوات واجهة مستخدم من Google يمكن استخدامها لإنشاء تطبيقات للأجهزة الجوّالة والويب وأجهزة سطح المكتب من خلال قاعدة رموز برمجية واحدة. في هذا الدرس العملي، ستنشئ تطبيق Flutter التالي:

يولّد التطبيق أسماءً جذابة، مثل "newstay" أو "lightstream" أو "mainbrake" أو "graypine". يمكن للمستخدم طلب الاسم التالي، وإضافة الاسم الحالي إلى قائمة الأسماء المفضّلة، ومراجعة قائمة الأسماء المفضّلة في صفحة منفصلة. يتوافق التطبيق مع أحجام الشاشات المختلفة.

المُعطيات

  • أساسيات طريقة عمل Flutter
  • إنشاء تنسيقات في Flutter
  • ربط تفاعلات المستخدمين (مثل الضغط على الأزرار) بسلوك التطبيق
  • الحفاظ على تنظيم رمز Flutter البرمجي
  • جعل تطبيقك سريع الاستجابة (للشاشات المختلفة)
  • الحصول على مظهر ومضمون متسقَين لتطبيقك

ستبدأ بإطار أساسي لتتمكّن من الانتقال مباشرةً إلى الأجزاء المهمة.

e9c6b402cd8003fd.png

في ما يلي فيديو يقدّمه "فيليب" يشرح فيه الخطوات الكاملة في هذا الدرس العملي.

انقر على "التالي" لبدء المعمل.

2. إعداد بيئة Flutter

محرِّر

لتبسيط هذا الدرس البرمجي قدر الإمكان، نفترض أنّك ستستخدم Visual Studio Code (VS Code) كبيئة تطوير. وهي مجانية وتعمل على جميع الأنظمة الأساسية الرئيسية.

بالطبع، يمكنك استخدام أي محرِّر تريده، مثل "استوديو Android" أو بيئات تطوير متكاملة أخرى من IntelliJ أو Emacs أو Vim أو Notepad++، فكلها تعمل مع Flutter.

ننصحك باستخدام VS Code لهذا الدرس التطبيقي لأنّ التعليمات تستخدم تلقائيًا اختصارات خاصة بـ VS Code. من الأسهل قول عبارات مثل "انقر هنا" أو "اضغط على هذا المفتاح" بدلاً من عبارات مثل "اتّخِذ الإجراء المناسب في المحرّر لتنفيذ X".

228c71510a8e868.png

اختيار هدف تطوير

‫Flutter هي مجموعة أدوات متعددة الأنظمة الأساسية. يمكن تشغيل تطبيقك على أي من أنظمة التشغيل التالية:

  • iOS
  • Android
  • Windows
  • نظام التشغيل Mac
  • Linux
  • الويب

ومع ذلك، من الممارسات الشائعة اختيار نظام تشغيل واحد ستجري عليه عملية التطوير بشكل أساسي. هذا هو "هدف التطوير"، أي نظام التشغيل الذي يعمل عليه تطبيقك أثناء عملية التطوير.

16695777c07f18e5.png

على سبيل المثال، لنفترض أنّك تستخدم جهاز كمبيوتر محمول يعمل بنظام التشغيل Windows لتطوير تطبيق Flutter. إذا اخترت Android كهدف التطوير، عليك عادةً توصيل جهاز Android بجهاز الكمبيوتر المحمول الذي يعمل بنظام التشغيل Windows باستخدام كابل USB، وسيتم تشغيل التطبيق قيد التطوير على جهاز Android المتصل. ولكن يمكنك أيضًا اختيار Windows كهدف التطوير، ما يعني أنّ تطبيقك قيد التطوير يعمل كتطبيق Windows إلى جانب المحرّر.

قد يكون من المغري اختيار الويب كهدف تطوير. أما عيب هذا الخيار فهو فقدان إحدى ميزات التطوير الأكثر فائدة في Flutter، وهي Stateful Hot Reload. لا يمكن لـ Flutter إعادة التحميل السريع لتطبيقات الويب.

يُرجى تحديد اختيارك الآن. تذكَّر أنّه يمكنك دائمًا تشغيل تطبيقك على أنظمة تشغيل أخرى لاحقًا. كل ما في الأمر أنّ تحديد هدف واضح للتطوير يسهّل الخطوة التالية.

تثبيت Flutter

تتوفّر دائمًا أحدث التعليمات حول كيفية تثبيت حزمة تطوير البرامج (SDK) من Flutter على docs.flutter.dev.

لا تغطي التعليمات الواردة على موقع Flutter الإلكتروني تثبيت حزمة SDK نفسها فحسب، بل أيضًا الأدوات ذات الصلة بالهدف من التطوير والمكوّنات الإضافية للمحرّر. تذكَّر أنّه في هذا الدرس العملي، عليك تثبيت ما يلي فقط:

  1. حزمة تطوير البرامج (SDK) في Flutter
  2. محرِّر Visual Studio Code مع المكوّن الإضافي Flutter
  3. البرامج المطلوبة لهدف التطوير الذي اخترته (على سبيل المثال: Visual Studio لاستهداف نظام التشغيل Windows أو Xcode لاستهداف نظام التشغيل macOS)

في القسم التالي، ستنشئ مشروع Flutter الأول.

إذا واجهتك مشاكل حتى الآن، قد تجد بعض هذه الأسئلة والأجوبة (من StackOverflow) مفيدة لتحديد المشاكل وحلّها.

الأسئلة الشائعة

3- إنشاء مشروع

إنشاء مشروع Flutter الأول

شغِّل Visual Studio Code وافتح لوحة الأوامر (باستخدام F1 أو Ctrl+Shift+P أو Shift+Cmd+P). ابدأ بكتابة "flutter new". اختَر الأمر Flutter: مشروع جديد.

بعد ذلك، اختَر تطبيق ثم مجلدًا لإنشاء مشروعك فيه. يمكن أن يكون هذا المسار هو دليل المنزل أو مسارًا مثل C:\src\.

أخيرًا، امنح مشروعك اسمًا. شيء مثل namer_app أو my_awesome_namer

260a7d97f9678005.png

ينشئ Flutter الآن مجلد مشروعك ويفتحه VS Code.

ستتم الآن الكتابة فوق محتوى 3 ملفات باستخدام بنية أساسية للتطبيق.

نسخ التطبيق الأوّلي ولصقه

في اللوحة اليمنى من VS Code، تأكَّد من اختيار المستكشف (Explorer)، وافتح الملف pubspec.yaml.

e2a5bab0be07f4f7.png

استبدِل محتوى هذا الملف بما يلي:

pubspec.yaml

name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter
  english_words: ^4.0.0
  provider: ^6.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0

flutter:
  uses-material-design: true

يحدّد ملف pubspec.yaml المعلومات الأساسية عن تطبيقك، مثل الإصدار الحالي والتبعيات والأصول التي سيتم تضمينها في الحزمة.

بعد ذلك، افتح ملف إعداد آخر في المشروع، analysis_options.yaml.

a781f218093be8e0.png

استبدِل محتواه بما يلي:

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_final_fields: false
    unnecessary_breaks: true
    use_key_in_widget_constructors: false

يحدّد هذا الملف مدى صرامة Flutter عند تحليل الرمز البرمجي. بما أنّ هذه هي تجربتك الأولى مع Flutter، فإنّك تطلب من أداة التحليل عدم التشدّد. ويمكنك تعديل هذا الإعداد في أي وقت لاحق. في الواقع، كلما اقتربت من نشر تطبيق فعلي، ستحتاج بالتأكيد إلى جعل أداة التحليل أكثر صرامة من ذلك.

أخيرًا، افتح الملف main.dart ضمن الدليل lib/.

e54c671c9bb4d23d.png

استبدِل محتوى هذا الملف بما يلي:

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
      ),
    );
  }
}

تشكّل أسطر الرمز البرمجي الخمسون هذه التطبيق بأكمله حتى الآن.

في القسم التالي، شغِّل التطبيق في وضع تصحيح الأخطاء وابدأ في تطويره.

4. إضافة زر

تضيف هذه الخطوة زر التالي لإنشاء زوج كلمات جديد.

تشغيل التطبيق

أولاً، افتح lib/main.dart وتأكَّد من اختيار الجهاز المستهدَف. في أسفل يسار نافذة VS Code، ستجد زرًا يعرض الجهاز المستهدف الحالي. انقر لتغييره.

أثناء فتح lib/main.dart، ابحث عن زر "تشغيل" b0a5d0200af5985d.png في أعلى يسار نافذة VS Code وانقر عليه.

بعد دقيقة تقريبًا، يتم تشغيل تطبيقك في وضع تصحيح الأخطاء. لا يبدو أنّ هناك الكثير حتى الآن:

f96e7dfb0937d7f4.png

أول عملية إعادة تحميل سريعة

في أسفل lib/main.dart، أضِف شيئًا إلى السلسلة في كائن Text الأول، واحفظ الملف (باستخدام Ctrl+S أو Cmd+S). على سبيل المثال:

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

لاحظ كيف يتغيّر التطبيق على الفور ولكن الكلمة العشوائية تظل كما هي. هذه هي ميزة إعادة التحميل السريع مع حفظ الحالة الشهيرة في Flutter. يتم تشغيل ميزة "إعادة التحميل السريع" عند حفظ التغييرات في ملف مصدر.

الأسئلة الشائعة

إضافة زر

بعد ذلك، أضِف زرًا في أسفل Column، أسفل مثيل Text الثاني مباشرةً.

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

عند حفظ التغيير، يتم تحديث التطبيق مرة أخرى: يظهر زر، وعند النقر عليه، تعرض وحدة تصحيح الأخطاء في VS Code الرسالة تم النقر على الزر!.

دورة مكثّفة عن Flutter في 5 دقائق

على الرغم من أنّ مشاهدة وحدة التحكّم في تصحيح الأخطاء ممتعة، إلا أنّك تريد أن يؤدي الزر وظيفة أكثر أهمية. قبل الانتقال إلى ذلك، ألقِ نظرة فاحصة على الرمز في lib/main.dart لفهم طريقة عمله.

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

في أعلى الملف، ستجد الدالة main(). في شكله الحالي، يطلب من Flutter تشغيل التطبيق المحدّد في MyApp فقط.

lib/main.dart

// ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

تمتد الفئة MyApp إلى StatelessWidget. التطبيقات المصغّرة هي العناصر التي يمكنك من خلالها إنشاء كل تطبيقات Flutter. وكما ترى، حتى التطبيق نفسه هو تطبيق مصغّر.

يعمل الرمز البرمجي في MyApp على إعداد التطبيق بأكمله، إذ ينشئ حالة التطبيق بأكمله (سنتحدّث عن ذلك لاحقًا)، ويسمّي التطبيق، ويحدّد المظهر المرئي، ويضبط أداة "الصفحة الرئيسية"، أي نقطة البداية لتطبيقك.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

بعد ذلك، تحدّد الفئة MyAppState حالة التطبيق. هذه هي تجربتك الأولى مع Flutter، لذا سنبقي هذا الدرس البرمجي بسيطًا ومركّزًا. تتوفّر العديد من الطرق الفعّالة لإدارة حالة التطبيق في Flutter. إحدى أسهل الطرق التي يمكن شرحها هي ChangeNotifier، وهي الطريقة التي يتّبعها هذا التطبيق.

  • تحدّد MyAppState البيانات التي يحتاجها التطبيق ليعمل. في الوقت الحالي، يحتوي على متغيّر واحد فقط يتضمّن زوج الكلمات العشوائي الحالي. ستضيف إلى هذا لاحقًا.
  • توسّع فئة الحالة ChangeNotifier، ما يعني أنّه يمكنها إبلاغ الآخرين بالتغييرات التي تطرأ عليها. على سبيل المثال، إذا تغيّر زوج الكلمات الحالي، تحتاج بعض التطبيقات المصغّرة في التطبيق إلى معرفة ذلك.
  • يتم إنشاء الحالة وتوفيرها للتطبيق بأكمله باستخدام ChangeNotifierProvider (راجِع الرمز أعلاه في MyApp). يتيح ذلك لأي أداة في التطبيق الحصول على الحالة.

d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           //  1
    var appState = context.watch<MyAppState>();  //  2

    return Scaffold(                             //  3
      body: Column(                              //  4
        children: [
          Text('A random AWESOME idea:'),        //  5
          Text(appState.current.asLowerCase),    //  6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       //  7
      ),
    );
  }
}

// ...

أخيرًا، هناك MyHomePage، وهي الأداة التي عدّلتها من قبل. يتوافق كل سطر مرقّم أدناه مع تعليق رقم السطر في الرمز أعلاه:

  1. يحدّد كل عنصر واجهة مستخدم طريقة build() يتم استدعاؤها تلقائيًا في كل مرة تتغير فيها ظروف عنصر واجهة المستخدم، وذلك لضمان أن يكون عنصر واجهة المستخدم محدّثًا دائمًا.
  2. تتتبّع MyHomePage التغييرات التي تطرأ على الحالة الحالية للتطبيق باستخدام الطريقة watch.
  3. يجب أن تعرض كل طريقة build تطبيقًا مصغّرًا أو (عادةً) شجرة متداخلة من التطبيقات المصغّرة. في هذه الحالة، يكون التطبيق المصغّر ذو المستوى الأعلى هو Scaffold. لن تعمل مع Scaffold في هذا الدرس البرمجي، ولكنّه أداة مفيدة ويمكن العثور عليها في الغالبية العظمى من تطبيقات Flutter الواقعية.
  4. Column هي إحدى أبسط أدوات تخطيط واجهة المستخدم في Flutter. تأخذ أي عدد من العناصر الفرعية وتضعها في عمود من أعلى إلى أسفل. تضع هذه السمة تلقائيًا العناصر الثانوية في أعلى العمود. ستغيّر هذا الإعداد قريبًا ليتم توسيط العمود.
  5. لقد غيّرت أداة Text هذه في الخطوة الأولى.
  6. يأخذ التطبيق المصغّر الثاني Text appState، ويصل إلى العضو الوحيد في هذه الفئة، وهو current (وهو WordPair). يوفّر WordPair العديد من دوال الجلب المفيدة، مثل asPascalCase أو asSnakeCase. نستخدم هنا asLowerCase، ولكن يمكنك تغيير ذلك الآن إذا كنت تفضّل أحد البدائل.
  7. لاحظ كيف أنّ رمز Flutter يستخدم الفواصل اللاحقة بشكل كبير. لا حاجة إلى هذه الفاصلة تحديدًا، لأنّ children هو العنصر الأخير (والوحيد أيضًا) في قائمة معلَمة Column هذه. ومع ذلك، من المستحسن عمومًا استخدام الفواصل اللاحقة: فهي تسهّل إضافة المزيد من الأعضاء، كما أنّها تعمل كتلميح لبرنامج التنسيق التلقائي في Dart لوضع سطر جديد هناك. لمزيد من المعلومات، يُرجى الاطّلاع على تنسيق الرمز.

بعد ذلك، عليك ربط الزر بالحالة.

سلوكك الأول

انتقِل إلى MyAppState وأضِف طريقة getNext.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  //  Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

تعيد الطريقة الجديدة getNext() تعيين current باستخدام WordPair عشوائي جديد. تتضمّن أيضًا الاتصال برقم notifyListeners()(وهي طريقة ChangeNotifier) تضمن إشعار أي شخص يشاهد MyAppState).

كل ما تبقى هو استدعاء الطريقة getNext من دالة معاودة الاتصال الخاصة بالزر.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

احفظ التغييرات وجرِّب التطبيق الآن. يجب أن تنشئ هذه الأداة زوجًا جديدًا من الكلمات العشوائية في كل مرة تضغط فيها على الزر التالي.

في القسم التالي، ستعمل على تحسين مظهر واجهة المستخدم.

5- تحسين مظهر التطبيق

هذا هو شكل التطبيق في الوقت الحالي.

3dd8a9d8653bdc56.png

ليس جيدًا. يجب أن يكون الجزء الرئيسي من التطبيق، أي زوج الكلمات الذي يتم إنشاؤه عشوائيًا، أكثر وضوحًا. فهو السبب الرئيسي الذي يدفع المستخدمين إلى استخدام هذا التطبيق. بالإضافة إلى ذلك، يظهر محتوى التطبيق بشكل غير متناسق في المنتصف، كما أنّ التطبيق بأكمله مملّ باللونين الأبيض والأسود.

يتناول هذا القسم هذه المشاكل من خلال العمل على تصميم التطبيق. الهدف النهائي لهذا القسم هو ما يلي:

2bbee054d81a3127.png

استخراج تطبيق مصغّر

يبدو السطر المسؤول عن عرض زوج الكلمات الحالي على النحو التالي الآن: Text(appState.current.asLowerCase). لتغييره إلى شيء أكثر تعقيدًا، من المستحسن استخراج هذا السطر إلى أداة منفصلة. يُعدّ توفير أدوات منفصلة للأجزاء المنطقية المنفصلة من واجهة المستخدم طريقة مهمة لإدارة التعقيد في Flutter.

توفّر Flutter أداة مساعدة لإعادة تصميم الرموز البرمجية من أجل استخراج الأدوات، ولكن قبل استخدامها، تأكَّد من أنّ السطر الذي يتم استخراجه لا يصل إلا إلى ما يحتاج إليه. في الوقت الحالي، يمكن للسطر الوصول إلى appState، ولكنّه يحتاج فقط إلى معرفة زوج الكلمات الحالي.

لهذا السبب، أعِد كتابة أداة MyHomePage على النحو التالي:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;                 //  Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                //  Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

جميل لم تعُد أداة Text تشير إلى appState بأكمله.

الآن، استدعِ قائمة إعادة الهيكلة. في VS Code، يمكنك إجراء ذلك بإحدى الطريقتَين التاليتَين:

  1. انقر بزر الماوس الأيمن على جزء الرمز الذي تريد إعادة تركيبه (Text في هذه الحالة) واختَر إعادة التركيب... من القائمة المنسدلة.

أو

  1. حرِّك المؤشر إلى رمز الجزء الذي تريد إعادة تصنيعه (Text في هذه الحالة)، واضغط على Ctrl+. (في نظام التشغيل Windows أو Linux) أو Cmd+. (في نظام التشغيل Mac).

في قائمة إعادة البناء، اختَر استخراج أداة. عيِّن اسمًا، مثل BigCard، وانقر على Enter.

يؤدي ذلك إلى إنشاء صف جديد تلقائيًا، BigCard، في نهاية الملف الحالي. يبدو الصف على النحو التالي:

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({super.key, required this.pair});

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

لاحظ كيف يواصل التطبيق العمل حتى أثناء إعادة البناء هذه.

إضافة بطاقة

حان الوقت الآن لتحويل هذه الأداة الجديدة إلى عنصر واجهة مستخدم بارز كما تخيّلنا في بداية هذا القسم.

ابحث عن الفئة BigCard والطريقة build() بداخلها. كما كان من قبل، استدعِ قائمة إعادة تصميم الرمز في الأداة Text. ومع ذلك، لن تستخرج التطبيق المصغّر هذه المرة.

بدلاً من ذلك، اختَر التفاف مع ترك مساحة فارغة. يؤدي ذلك إلى إنشاء أداة رئيسية جديدة حول الأداة Text باسم Padding. بعد الحفظ، ستلاحظ أنّ الكلمة العشوائية أصبحت تتضمّن مساحة أكبر.

زيادة مساحة الحشو عن القيمة التلقائية 8.0 على سبيل المثال، استخدِم شيئًا مثل 20 للحصول على مساحة متروكة أكبر.

بعد ذلك، انتقِل إلى مستوى أعلى. ضَع مؤشر الماوس على التطبيق المصغّر Padding، وافتح قائمة إعادة تصميم الرمز، ثم اختَر تضمين التطبيق المصغّر....

يتيح لك ذلك تحديد الأداة الرئيسية. اكتب "بطاقة" واضغط على Enter.

يغلّف هذا الرمز أداة Padding، وبالتالي يغلّف أيضًا Text، باستخدام أداة Card.

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({super.key, required this.pair});

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }
}

// ...

سيظهر التطبيق الآن على النحو التالي:

6031adbc0a11e16b.png

المظهر والأسلوب

لجعل البطاقة أكثر تميزًا، يمكنك تلوينها بلون أكثر ثراءً. ولأنّه من الجيد دائمًا الحفاظ على نظام ألوان متّسق، استخدِم Theme في التطبيق لاختيار اللون.

أجرِ التغييرات التالية على طريقة build() في BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       //  Add this.

    return Card(
      color: theme.colorScheme.primary,    //  And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

يؤدي هذان السطران الجديدان الكثير من العمل:

  • أولاً، يطلب الرمز البرمجي مظهر التطبيق الحالي باستخدام Theme.of(context).
  • بعد ذلك، يحدّد الرمز لون البطاقة ليكون هو نفسه قيمة السمة colorScheme. يتضمّن نظام الألوان العديد من الألوان، وprimary هو اللون الأبرز الذي يحدّد لون التطبيق.

تم الآن تلوين البطاقة باللون الأساسي للتطبيق:

a136f7682c204ea1.png

يمكنك تغيير هذا اللون ونظام الألوان للتطبيق بأكمله من خلال الانتقال للأعلى إلى MyApp وتغيير لون البداية في ColorScheme.

لاحظ كيف تتحرك الألوان بسلاسة. يُطلق على ذلك اسم الرسوم المتحركة الضمنية. ستعمل العديد من عناصر واجهة المستخدم في Flutter على إدخال القيم بسلاسة حتى لا "تقفز" واجهة المستخدم بين الحالات.

يتغيّر أيضًا لون الزر البارز أسفل البطاقة. هذه هي ميزة استخدام Theme على مستوى التطبيق بدلاً من ترميز القيم بشكل ثابت.

TextTheme

لا تزال البطاقة تتضمّن مشكلة: النص صغير جدًا ويصعب قراءة لونه. لحلّ هذه المشكلة، عليك إجراء التغييرات التالية على طريقة build() في BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    //  Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        //  Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

سبب هذا التغيير:

  • باستخدام theme.textTheme,، يمكنك الوصول إلى مظهر خط التطبيق. يتضمّن هذا الصف عناصر مثل bodyMedium (للنص العادي ذي الحجم المتوسط) أو caption (لتعليقات الصور) أو headlineLarge (للعناوين الكبيرة).
  • السمة displayMedium هي نمط كبير مخصّص لعرض النص. يُستخدم مصطلح عرض هنا بالمعنى الطباعي، كما هو الحال في خط العرض. تذكر مستندات displayMedium أنّ "أنماط العرض مخصّصة للنصوص القصيرة والمهمة"، وهذا هو بالضبط ما نريد تحقيقه.
  • يمكن نظريًا أن تكون قيمة السمة displayMedium للمظهر هي null. لغة البرمجة Dart التي تكتب بها هذا التطبيق هي لغة آمنة من القيم الخالية، لذا لن تسمح لك باستدعاء طرق الكائنات التي قد تكون null. في هذه الحالة، يمكنك استخدام عامل التشغيل ! ("عامل التشغيل bang") لتأكيد معرفتك بما تفعله في Dart. (displayMedium ليس قيمة فارغة بالتأكيد في هذه الحالة. لا يهمّنا في هذا الدرس التدريبي معرفة السبب وراء ذلك.)
  • يؤدي استدعاء copyWith() على displayMedium إلى عرض نسخة من نمط النص مع التغييرات التي تحدّدها. في هذه الحالة، أنت تغيّر لون النص فقط.
  • للحصول على اللون الجديد، عليك الوصول إلى مظهر التطبيق مرة أخرى. تحدّد السمة onPrimary لنظام الألوان لونًا مناسبًا للاستخدام على اللون الأساسي للتطبيق.

من المفترض أن يظهر التطبيق الآن على النحو التالي:

2405e9342d28c193.png

إذا أردت، يمكنك إجراء المزيد من التغييرات على البطاقة. وفي ما يلي بعض الأفكار:

  • يتيح لك الرمز copyWith() تغيير الكثير من المعلومات حول نمط النص، وليس اللون فقط. للحصول على القائمة الكاملة بالسمات التي يمكنك تغييرها، ضَع مؤشر الماوس في أي مكان داخل الأقواس الخاصة بـ copyWith()، ثم اضغط على Ctrl+Shift+Space (في نظام التشغيل Windows أو Linux) أو Cmd+Shift+Space (في نظام التشغيل Mac).
  • وبالمثل، يمكنك تغيير المزيد من المعلومات حول التطبيق المصغّر Card. على سبيل المثال، يمكنك تكبير ظل البطاقة من خلال زيادة قيمة المَعلمة elevation.
  • جرِّب استخدام ألوان مختلفة. بالإضافة إلى theme.colorScheme.primary، هناك أيضًا .secondary و.surface وغير ذلك الكثير. تحتوي جميع هذه الألوان على مكافئاتها onPrimary.

تحسين إمكانية الوصول

تتيح Flutter إمكانية الوصول إلى التطبيقات تلقائيًا. على سبيل المثال، يعرض كل تطبيق Flutter بشكل صحيح جميع النصوص والعناصر التفاعلية في التطبيق لبرامج قراءة الشاشة، مثل TalkBack وVoiceOver.

d1fad7944fb890ea.png

ومع ذلك، قد تحتاج أحيانًا إلى إجراء بعض التعديلات. في حالة هذا التطبيق، قد يواجه قارئ الشاشة مشاكل في نطق بعض أزواج الكلمات التي تم إنشاؤها. في حين لا يواجه البشر مشاكل في تحديد الكلمتَين في cheaphead، قد يلفظ قارئ الشاشة ph في منتصف الكلمة على النحو f.

الحلّ هو استبدال pair.asLowerCase بـ "${pair.first} ${pair.second}". يستخدم الأخير عملية استيفاء السلسلة لإنشاء سلسلة (مثل "cheap head") من الكلمتَين الواردتَين في pair. يضمن استخدام كلمتين منفصلتين بدلاً من كلمة مركّبة أن تتعرّف برامج قراءة الشاشة عليهما بشكل صحيح، كما يوفّر تجربة أفضل للمستخدمين الذين يعانون ضعفًا في النظر.

ومع ذلك، قد تحتاج إلى الحفاظ على البساطة المرئية في pair.asLowerCase. استخدِم السمة Text مع الخاصية semanticsLabel لتجاوز المحتوى المرئي لعنصر واجهة المستخدم النصي بمحتوى دلالي أكثر ملاءمة لأجهزة قراءة الشاشة:

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        //  Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

الآن، تنطق برامج قراءة الشاشة كل زوج من الكلمات التي تم إنشاؤها بشكل صحيح، ولكن تظل واجهة المستخدم كما هي. جرِّب ذلك عمليًا من خلال استخدام قارئ شاشة على جهازك.

توسيط واجهة المستخدم

بعد أن تم عرض زوج الكلمات العشوائي مع قدر كافٍ من الجاذبية البصرية، حان الوقت لوضعه في وسط نافذة/شاشة التطبيق.

أولاً، تذكَّر أنّ BigCard هو جزء من Column. تجمع الأعمدة تلقائيًا العناصر الفرعية في الأعلى، ولكن يمكننا إلغاء هذا الإعداد. انتقِل إلى طريقة MyHomePage في build() وأجرِ التغيير التالي:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  //  Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

يؤدي ذلك إلى توسيط العناصر الثانوية داخل Column على طول المحور الرئيسي (العمودي).

b555d4c7f5000edf.png

تكون العناصر الفرعية في المنتصف على طول المحور المتقاطع للعمود (أي أنّها تكون في المنتصف أفقيًا). ولكن Column نفسه ليس في وسط Scaffold. يمكننا التحقّق من ذلك باستخدام أداة فحص التطبيقات المصغّرة.

لا يندرج "فاحص التطبيقات المصغّرة" نفسه ضمن نطاق هذا الدرس العملي، ولكن يمكنك ملاحظة أنّه عند تمييز Column، لا يشغل العرض الكامل للتطبيق، بل يشغل فقط المساحة الأفقية التي تحتاج إليها العناصر التابعة له.

يمكنك توسيط العمود نفسه. ضَع مؤشر الماوس على Column، وافتح قائمة إعادة التركيب (باستخدام Ctrl+. أو Cmd+.)، ثم اختَر التضمين في Center.

من المفترض أن يظهر التطبيق الآن على النحو التالي:

455688d93c30d154.png

يمكنك تعديل هذا الإعداد أكثر إذا أردت.

  • يمكنك إزالة أداة Text أعلاه BigCard. يمكن القول إنّ النص الوصفي ("فكرة رائعة عشوائية:") لم يعُد ضروريًا لأنّ واجهة المستخدم مفهومة حتى بدونه. وهذه الطريقة أكثر نظافة.
  • يمكنك أيضًا إضافة أداة SizedBox(height: 10) بين BigCard وElevatedButton. بهذه الطريقة، سيكون هناك فاصل أكبر بين التطبيقَين المصغّرَين. لا يشغل التطبيق المصغّر SizedBox سوى مساحة ولا يعرض أي محتوى بمفرده. ويُستخدَم عادةً لإنشاء "فجوات" مرئية.

مع التغييرات الاختيارية، يحتوي MyHomePage على الرمز التالي:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

ويظهر التطبيق على النحو التالي:

3d53d2b071e2f372.png

في القسم التالي، ستضيف إمكانية وضع علامة "مفضّلة" (أو "إعجاب") على الكلمات التي تم إنشاؤها.

6. إضافة وظائف

يعمل التطبيق، بل ويقدّم أحيانًا أزواج كلمات مثيرة للاهتمام. ولكن عندما ينقر المستخدم على التالي، يختفي كل زوج من الكلمات نهائيًا. من الأفضل توفير طريقة "لتذكُّر" أفضل الاقتراحات، مثل زر "أعجبني".

e6b01a8c90df8ffa.png

إضافة منطق النشاط التجاري

انتقِل إلى MyAppState وأضِف الرمز التالي:

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  //  Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

فحص التغييرات:

  • أضفت سمة جديدة إلى MyAppState باسم favorites. يتم ضبط قيمة هذه السمة على قائمة فارغة: [].
  • حدّدت أيضًا أنّه لا يمكن أن تحتوي القائمة إلا على أزواج من الكلمات: <WordPair>[]، باستخدام الأنواع العامة. يساعد ذلك في جعل تطبيقك أكثر قوة، إذ يرفض Dart حتى تشغيل تطبيقك إذا حاولت إضافة أي شيء آخر غير WordPair إليه. وبالتالي، يمكنك استخدام قائمة favorites مع العلم أنّه لا يمكن أن تكون هناك أي عناصر غير مرغوب فيها (مثل null) مخفية فيها.
  • أضفت أيضًا طريقة جديدة، toggleFavorite()، تزيل زوج الكلمات الحالي من قائمة الكلمات المفضّلة (إذا كان موجودًا فيها)، أو تضيفه (إذا لم يكن موجودًا بعد). في كلتا الحالتين، يستدعي الرمز notifyListeners(); بعد ذلك.

إضافة الزر

بعد الانتهاء من "منطق النشاط التجاري"، حان الوقت للعمل على واجهة المستخدم مرة أخرى. يتطلّب وضع زرّ "أعجبني" على يسار زرّ "التالي" Row. الأداة Row هي المكافئ الأفقي للأداة Column التي رأيتها سابقًا.

أولاً، ضع الزر الحالي في علامة Row. انتقِل إلى طريقة MyHomePage، وضَع مؤشر الماوس على build()، وافتح قائمة إعادة التركيب باستخدام Ctrl+. أو Cmd+.، ثم اختَر التضمين في صف.ElevatedButton

عند الحفظ، ستلاحظ أنّ Row يتصرف بشكل مشابه لـ Column، إذ يجمع العناصر الفرعية إلى اليمين تلقائيًا. (Column جمعت العناصر الفرعية في الأعلى). لحلّ هذه المشكلة، يمكنك اتّباع النهج نفسه كما في السابق، ولكن مع إضافة mainAxisAlignment. ومع ذلك، لأغراض تعليمية، استخدِم mainAxisSize. يخبر هذا الإعداد Row بعدم استخدام كل المساحة الأفقية المتاحة.

أجرِ التغيير التالي:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   //  Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

تعود واجهة المستخدم إلى ما كانت عليه من قبل.

3d53d2b071e2f372.png

بعد ذلك، أضِف زر أعجبني واربطه بـ toggleFavorite(). للاختبار، حاوِل إجراء ذلك بنفسك أولاً بدون النظر إلى مجموعة الرموز البرمجية أدناه.

e6b01a8c90df8ffa.png

لا بأس إذا لم تنفّذ ذلك بالطريقة نفسها الموضّحة أدناه. في الواقع، لا داعي للقلق بشأن رمز القلب إلا إذا كنت تريد حقًا مواجهة تحدٍ كبير.

لا بأس أيضًا إذا لم تنجح في ذلك، فهذه هي الساعة الأولى لك مع Flutter.

252f7c4a212c94d2.png

في ما يلي إحدى طرق إضافة الزر الثاني إلى MyHomePage. في هذه المرة، استخدِم الدالة الإنشائية ElevatedButton.icon() لإنشاء زر يتضمّن رمزًا. في أعلى طريقة build، اختَر الرمز المناسب حسب ما إذا كان زوج الكلمات الحالي مُدرجًا في المفضّلة. لاحظ أيضًا استخدام SizedBox مرة أخرى لإبقاء الزرّين متباعدَين قليلاً.

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    //  Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                //  And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

يجب أن يظهر التطبيق على النحو التالي:

للأسف، لا يمكن للمستخدم الاطّلاع على الأماكن المفضّلة. حان الوقت لإضافة شاشة منفصلة بالكامل إلى تطبيقنا. نراك في القسم التالي.

7. إضافة شريط تنقّل

لا يمكن لمعظم التطبيقات عرض كل المحتوى في شاشة واحدة. من المحتمل أنّ هذا التطبيق يمكنه ذلك، ولكن لأغراض تعليمية، ستنشئ شاشة منفصلة للمفضّلة لدى المستخدم. للتبديل بين الشاشتين، عليك تنفيذ أول StatefulWidget.

f62c54f5401a187.png

للوصول إلى جوهر هذه الخطوة في أقرب وقت ممكن، قسِّم MyHomePage إلى أداتَين منفصلتَين.

اختَر كل MyHomePage، واحذفه، ثم استبدله بالرمز التالي:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

عند الحفظ، ستلاحظ أنّ الجانب المرئي من واجهة المستخدم جاهز، ولكنّه لا يعمل. لا يؤدي النقر على رمز ♥︎ (القلب) في شريط التنقّل إلى أي إجراء.

388bc25fe198c54a.png

فحص التغييرات

  • أولاً، لاحظ أنّه يتم استخراج كل محتوى MyHomePage إلى أداة جديدة، وهي GeneratorPage. الجزء الوحيد من أداة MyHomePage القديمة الذي لم يتم استخراجه هو Scaffold.
  • تحتوي MyHomePage الجديدة على Row مع طفلين. الأداة الأولى هي SafeArea، والأداة الثانية هي أداة Expanded.
  • يضمن SafeArea عدم حجب العنصر الثانوي بواسطة فتحة في الجهاز أو شريط الحالة. في هذا التطبيق، يلتف العنصر حول NavigationRail لمنع إخفاء أزرار التنقّل بشريط الحالة على الجهاز الجوّال، على سبيل المثال.
  • يمكنك تغيير سطر extended: false في NavigationRail إلى true. يؤدي ذلك إلى عرض التصنيفات بجانب الرموز. في خطوة لاحقة، ستتعرّف على كيفية إجراء ذلك تلقائيًا عندما يتوفّر للتطبيق مساحة أفقية كافية.
  • يحتوي شريط التنقّل على وجهتَين (الصفحة الرئيسية والمفضّلة)، مع الرموز والتصنيفات الخاصة بكل منهما. ويحدّد أيضًا selectedIndex الحالي. يؤدي اختيار فهرس بقيمة صفر إلى تحديد الوجهة الأولى، ويؤدي اختيار فهرس بقيمة واحد إلى تحديد الوجهة الثانية، وهكذا. في الوقت الحالي، تم ضبطها على صفر.
  • يحدّد شريط التنقّل أيضًا ما يحدث عندما يختار المستخدم أحد وجهات onDestinationSelected. في الوقت الحالي، يعرض التطبيق قيمة الفهرس المطلوبة فقط باستخدام print().
  • العنصر الثانوي الثاني من Row هو الأداة Expanded. تكون الأدوات الموسّعة مفيدة للغاية في الصفوف والأعمدة، فهي تتيح لك التعبير عن التصاميم التي تشغل فيها بعض العناصر المساحة التي تحتاج إليها فقط (SafeArea في هذه الحالة)، بينما تشغل الأدوات الأخرى أكبر قدر ممكن من المساحة المتبقية (Expanded في هذه الحالة). إحدى الطرق التي يمكن من خلالها فهم Expanded الأدوات هي أنّها "طماعة". إذا أردت فهم دور هذا التطبيق المصغّر بشكل أفضل، جرِّب تضمين التطبيق المصغّر SafeArea في تطبيق مصغّر آخر Expanded. يبدو التنسيق الناتج على النحو التالي:

6bbda6c1835a1ae.png

  • يقسّم عنصرَا Expanded واجهة المستخدم كل المساحة الأفقية المتاحة بينهما، على الرغم من أنّ شريط التنقّل يحتاج فقط إلى جزء صغير على اليمين.
  • داخل الأداة Expanded، هناك Container ملون، وداخل الحاوية، هناك GeneratorPage.

التطبيقات المصغّرة التي لا تعتمد على الحالة مقابل التطبيقات المصغّرة التي تعتمد على الحالة

حتى الآن، كان تطبيق MyAppState يلبّي جميع احتياجاتك في ولايتك. لهذا السبب، تكون جميع التطبيقات المصغّرة التي كتبتها حتى الآن بلا حالة. ولا تحتوي على أي حالة قابلة للتغيير خاصة بها. لا يمكن لأي من التطبيقات المصغّرة تغيير نفسها، بل يجب أن تمرّ عبر MyAppState.

لكنّ هذا الأمر سيتغيّر قريبًا.

تحتاج إلى طريقة للاحتفاظ بقيمة selectedIndex في شريط التنقّل. يجب أن تتمكّن أيضًا من تغيير هذه القيمة من داخل دالة الاستدعاء onDestinationSelected.

يمكنك إضافة selectedIndex كسمة أخرى من سمات MyAppState. وكانت تنجح. ولكن يمكنك أن تتخيل أنّ حالة التطبيق ستتجاوز الحدّ المعقول بسرعة إذا كان كل تطبيق مصغّر يخزّن قيمه فيها.

e52d9c0937cc0823.jpeg

بعض الحالات ذات صلة بأداة واحدة فقط، لذا يجب أن تبقى مع هذه الأداة.

أدخِل StatefulWidget، وهو نوع من التطبيقات المصغّرة التي تتضمّن State. أولاً، حوِّل MyHomePage إلى أداة ذات حالة.

ضَع مؤشر الماوس على السطر الأول من MyHomePage (السطر الذي يبدأ بـ class MyHomePage...)، وافتح قائمة إعادة تصميم الرمز باستخدام Ctrl+. أو Cmd+.. بعد ذلك، اختَر التحويل إلى StatefulWidget.

تنشئ بيئة التطوير المتكاملة صفًا جديدًا لك، وهو _MyHomePageState. توسّع هذه الفئة State، وبالتالي يمكنها إدارة قيمها الخاصة. (يمكن أن يتغيّر تلقائيًا.) لاحظ أيضًا أنّ طريقة build من الأداة القديمة التي لا تحتفظ بحالتها قد تم نقلها إلى _MyHomePageState (بدلاً من البقاء في الأداة). تم نقلها كما هي، ولم يتغيّر أي شيء داخل طريقة build. وهي الآن متوفّرة في مكان آخر.

setState

يحتاج التطبيق المصغَّر الجديد ذو الحالة إلى تتبُّع متغيّر واحد فقط: selectedIndex. أجرِ التغييرات الثلاثة التالية على _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     //  Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    //  Change to this.
              onDestinationSelected: (value) {

                //  Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

فحص التغييرات:

  1. يمكنك تقديم متغيّر جديد، selectedIndex، وتعيين قيمته الأولية إلى 0.
  2. يمكنك استخدام هذا المتغيّر الجديد في تعريف NavigationRail بدلاً من 0 المرمّز ثابتًا الذي كان متوفّرًا حتى الآن.
  3. عند استدعاء دالة معاودة الاتصال onDestinationSelected، بدلاً من مجرد طباعة القيمة الجديدة إلى وحدة التحكّم، يمكنك تعيينها إلى selectedIndex داخل استدعاء setState(). تشبه هذه المكالمة طريقة notifyListeners() المستخدَمة سابقًا، فهي تضمن تعديل واجهة المستخدم.

يستجيب شريط التنقّل الآن لتفاعل المستخدم. لكنّ المساحة الموسّعة على اليمين تبقى كما هي. ويرجع ذلك إلى أنّ الرمز لا يستخدم selectedIndex لتحديد الشاشة التي يتم عرضها.

استخدام selectedIndex

ضَع الرمز التالي في أعلى طريقة build الخاصة بـ _MyHomePageState، قبل return Scaffold مباشرةً:

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

اطّلِع على جزء الرمز البرمجي التالي:

  1. يعرّف الرمز متغيرًا جديدًا، page، من النوع Widget.
  2. بعد ذلك، تحدّد عبارة switch شاشة لعنصر page، وفقًا للقيمة الحالية في selectedIndex.
  3. بما أنّه لا يتوفّر FavoritesPage بعد، استخدِم Placeholder، وهو تطبيق مصغّر مفيد يرسم مستطيلاً متقاطعًا في أي مكان تضعه فيه، ما يشير إلى أنّ هذا الجزء من واجهة المستخدم لم يكتمل بعد.

5685cf886047f6ec.png

  1. من خلال تطبيق مبدأ الفشل السريع، يحرص بيان switch أيضًا على عرض خطأ إذا لم تكن قيمة selectedIndex هي 0 أو 1. يساعد ذلك في منع حدوث أخطاء في المستقبل. إذا أضفت وجهة جديدة إلى شريط التنقّل ونسيت تعديل هذا الرمز، سيتعطّل البرنامج أثناء التطوير (بدلاً من أن يتركك تخمّن سبب عدم عمل الأشياء أو يسمح لك بنشر رمز يتضمّن أخطاء في مرحلة الإنتاج).

بعد أن أصبح page يحتوي على الأداة التي تريد عرضها على اليسار، يمكنك على الأرجح تخمين التغيير الآخر المطلوب.

في ما يلي قيمة _MyHomePageState بعد إجراء هذا التغيير المتبقي:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  //  Here.
            ),
          ),
        ],
      ),
    );
  }
}

// ...

ينتقل التطبيق الآن بين GeneratorPage والعنصر النائب الذي سيصبح قريبًا صفحة المفضّلة.

سرعة الاستجابة

بعد ذلك، اجعل شريط التنقّل سريع الاستجابة. أي اجعلها تعرض التصنيفات تلقائيًا (باستخدام extended: true) عندما تتوفّر مساحة كافية لها.

a8873894c32e0d0b.png

توفّر Flutter العديد من الأدوات التي تساعدك في جعل تطبيقاتك متجاوبة تلقائيًا. على سبيل المثال، Wrap هي أداة مشابهة للأداة Row أو Column تعمل تلقائيًا على نقل العناصر الثانوية إلى "السطر" التالي (يُسمى "تشغيل") عندما لا تتوفّر مساحة عمودية أو أفقية كافية. هناك FittedBox، وهي أداة تتناسب تلقائيًا مع المساحة المتوفّرة وفقًا لمواصفاتك.

لكنّ NavigationRail لا يعرض التصنيفات تلقائيًا عندما تكون هناك مساحة كافية لأنّه لا يمكنه معرفة ما يعتبر مساحة كافية في كل سياق. ويعود إليك، أي المطوّر، اتّخاذ هذا القرار.

لنفترض أنّك قرّرت عرض التصنيفات فقط إذا كان عرض MyHomePage يبلغ 600 بكسل على الأقل.

التطبيق المصغّر الذي سيتم استخدامه في هذه الحالة هو LayoutBuilder. تتيح لك هذه السمة تغيير بنية التطبيق المصغّر استنادًا إلى المساحة المتوفرة لديك.

مرّة أخرى، استخدِم قائمة إعادة تصميم في Flutter في VS Code لإجراء التغييرات المطلوبة. في هذه المرة، الأمر أكثر تعقيدًا بعض الشيء:

  1. داخل طريقة build الخاصة بـ _MyHomePageState، ضَع مؤشر الماوس على Scaffold.
  2. استدعِ قائمة إعادة الهيكلة (Refactor) باستخدام ‎Ctrl+. (في نظام التشغيل Windows أو Linux) أو ‎Cmd+. (في نظام التشغيل Mac).
  3. اختَر التضمين في أداة الإنشاء واضغط على Enter.
  4. عدِّل اسم Builder الذي تمت إضافته مؤخرًا إلى LayoutBuilder.
  5. عدِّل قائمة مَعلمات دالة الرجوع من (context) إلى (context, constraints).

يتم استدعاء دالة معاودة الاتصال builder الخاصة بـ LayoutBuilder في كل مرة تتغير فيها القيود. يحدث ذلك مثلاً في الحالات التالية:

  • يغيّر المستخدم حجم نافذة التطبيق
  • يدوّر المستخدم هاتفه من الوضع العمودي إلى الوضع الأفقي أو العكس
  • تزداد مساحة بعض الأدوات بجانب MyHomePage، ما يؤدي إلى تقليل قيود MyHomePage

يمكن لرمزك الآن تحديد ما إذا كان سيتم عرض التصنيف من خلال طلب constraints الحالي. أدخِل التغيير التالي على سطر واحد في طريقة build الخاصة بـ _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  //  Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

أصبح تطبيقك الآن يستجيب لبيئته، مثل حجم الشاشة واتجاهها والنظام الأساسي. بعبارة أخرى، إنّها متجاوبة.

كل ما تبقى هو استبدال Placeholder بشاشة المفضّلة فعلية. سنتناول هذا الموضوع في القسم التالي.

‫8. إضافة صفحة جديدة

هل تذكر أداة Placeholder التي استخدمناها بدلاً من صفحة المفضّلة؟

حان الوقت لحلّ هذه المشكلة.

إذا كنت تحب المغامرة، جرِّب تنفيذ هذه الخطوة بنفسك. هدفك هو عرض قائمة favorites في أداة جديدة عديمة الحالة، FavoritesPage، ثم عرض هذه الأداة بدلاً من Placeholder.

إليك بعض الإرشادات:

  • عندما تريد Column قابلاً للتمرير، استخدِم أداة ListView.
  • تذكَّر أنّه يمكنك الوصول إلى مثيل MyAppState من أي أداة باستخدام context.watch<MyAppState>().
  • إذا كنت تريد أيضًا تجربة أداة جديدة، يحتوي ListTile على سمات مثل title (للنصوص بشكل عام) وleading (للرموز أو الأفاتارات) وonTap (للتفاعلات). ومع ذلك، يمكنك تحقيق تأثيرات مشابهة باستخدام التطبيقات المصغّرة التي تعرفها.
  • تتيح لغة Dart استخدام حلقات for داخل القيم الحرفية للمجموعات. على سبيل المثال، إذا كان messages يحتوي على قائمة سلاسل، يمكنك استخدام رمز برمجي مثل ما يلي:

f0444bba08f205aa.png

من ناحية أخرى، إذا كنت أكثر دراية بالبرمجة الوظيفية، يتيح لك Dart أيضًا كتابة الرمز البرمجي مثل messages.map((m) => Text(m)).toList(). وبالطبع، يمكنك دائمًا إنشاء قائمة بالتطبيقات المصغّرة وإضافتها بشكل إلزامي داخل طريقة build.

تتمثّل ميزة إضافة صفحة المفضّلة بنفسك في أنّك ستتعرّف على المزيد من المعلومات من خلال اتّخاذ قراراتك الخاصة. أما العيب فهو أنّك قد تواجه مشاكل لا يمكنك حلّها بنفسك. تذكَّر أنّ الفشل أمر طبيعي، وهو أحد أهم عناصر التعلّم. لا يتوقّع أحد أن تتقن تطوير تطبيقات Flutter في الساعة الأولى، ولا يجب أن تتوقّع ذلك من نفسك.

252f7c4a212c94d2.png

في ما يلي إحدى الطرق لتنفيذ صفحة المفضّلة. ستلهمك طريقة تنفيذها (نأمل ذلك) لتجربة الرمز البرمجي وتحسين واجهة المستخدِم وتخصيصها.

إليك فئة FavoritesPage الجديدة:

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

في ما يلي وظيفة الأداة:

  • يتم الحصول على الحالة الحالية للتطبيق.
  • إذا كانت قائمة المفضّلة فارغة، ستظهر رسالة في الوسط: لم تتم إضافة أي قنوات مفضّلة بعد.
  • بخلاف ذلك، سيتم عرض قائمة (قابلة للتمرير).
  • تبدأ القائمة بملخّص (على سبيل المثال، لديك 5 صور مفضّلة.).
  • بعد ذلك، تتكرّر التعليمات البرمجية في جميع العناصر المفضّلة، ويتم إنشاء تطبيق مصغّر ListTile لكل عنصر.

كل ما تبقى الآن هو استبدال التطبيق المصغّر Placeholder بتطبيق FavoritesPage. وهذا كل ما في الأمر.

يمكنك الحصول على الرمز النهائي لهذا التطبيق في مستودع الدرس التطبيقي حول الترميز على GitHub.

9- الخطوات التالية

تهانينا!

انظر إلى نفسك. لقد أخذت بنية أساسية غير فعّالة تتضمّن أداة Column وأداة Text، وحوّلتها إلى تطبيق صغير رائع ومتوافق مع مختلف الأجهزة.

d6e3d5f736411f13.png

المواضيع التي تناولناها

  • أساسيات طريقة عمل Flutter
  • إنشاء تنسيقات في Flutter
  • ربط تفاعلات المستخدمين (مثل الضغط على الأزرار) بسلوك التطبيق
  • الحفاظ على تنظيم رمز Flutter البرمجي
  • جعل تطبيقك متجاوبًا
  • الحصول على مظهر ومضمون متسقَين لتطبيقك

ما هي الخطوات التالية؟

  • جرِّب المزيد من الميزات في التطبيق الذي كتبته خلال هذا التدريب العملي.
  • اطّلِع على الرمز البرمجي لهذه النسخة المتقدّمة من التطبيق نفسه لمعرفة كيفية إضافة قوائم متحركة وتدرّجات لونية وتأثيرات تلاشي تدريجي وغير ذلك.
  • يمكنك متابعة رحلة التعلّم من خلال الانتقال إلى flutter.dev/learn.