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

1. مقدمة

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

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

المُعطيات

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

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

e9c6b402cd8003fd.png

في ما يلي فيديو يقدّمه فيليب يشرح فيه خطوات إنشاء المشروع بالكامل.

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

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

محرِّر

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

يمكنك بالطبع استخدام أي محرِّر تريده: Android Studio أو أدوات تطوير البرامج المتكاملة الأخرى من 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، وهي ميزة "إعادة التحميل السريع" التي تتضمّن حالة التطبيق. لا يمكن إعادة تحميل تطبيقات الويب بسرعة باستخدام Flutter.

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

تثبيت Flutter

يمكنك العثور على أحدث التعليمات حول كيفية تثبيت حزمة Flutter SDK على 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: مشروع جديد (Flutter: New Project).

بعد ذلك، اختَر التطبيق ثم مجلدًا لإنشاء مشروعك فيه. قد يكون هذا الدليل هو الدليل الرئيسي أو رمز مثل 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' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.6.0

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.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(
          useMaterial3: true,
          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(
          useMaterial3: true,
          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+. (Win/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.

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 (Win/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. استخدِم السمة semanticsLabel في Text لتجاهل المحتوى المرئي لأداة النص باستخدام محتوى دلالي أكثر ملاءمةً لأجهزة قراءة الشاشة:

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. تُجمِّع الأعمدة عناصرها الفرعية تلقائيًا في الأعلى، ولكن يمكننا إلغاء ذلك بسهولة. انتقِل إلى طريقة build() في 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: 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+.)، واختَر اللفّ باستخدام المركز.

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

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. Widget Row هو المكافئ الأفقي لـ Column الذي رأيته سابقًا.

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

عند الحفظ، ستلاحظ أنّ 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. بعد ذلك، تُحدِّد عبارة التبديل شاشة لـ page، وفقًا للقيمة الحالية في selectedIndex.
  3. بما أنّ رمز FavoritesPage غير متوفّر بعد، استخدِم رمز Placeholder، وهو تطبيق مصغّر مفيد يرسم مستطيلاً متقاطعًا في أي مكان تضعه فيه، ما يشير إلى أنّ هذا الجزء من واجهة المستخدم غير مكتمل.

5685cf886047f6ec.png

  1. من خلال تطبيق مبدأ الفشل السريع، تتأكّد عبارة التبديل أيضًا من طرح خطأ إذا لم تكن القيمة 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. ضَع مؤشر الماوس على Scaffold داخل طريقة build في _MyHomePageState.
  2. افتح قائمة إعادة التحليل باستخدام 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. وفويلا!

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

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

تهانينا!

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

d6e3d5f736411f13.png

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

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

ماذا بعد ذلك؟

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