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

1. مقدمة

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

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

ما ستتعرَّف عليه

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

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

e9c6b402cd8003fd.png

سيطلعك "فيليب" على الدروس التطبيقية حول الترميز.

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

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

محرِّر

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

لا مانع من استخدام أي محرّر تريده: "استوديو Android" أو برامج IntelliJ IDE أو 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 مع المكوّن الإضافي 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.

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

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

في الجزء الأيمن من رمز VS، تأكَّد من اختيار 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.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.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، سيظهر لك زرّ يعرض الجهاز المستهدَف الحالي. انقر لتغييرها.

عندما يكون 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'),
          ),

        ],
      ),
    );

// ...

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

دورة مكثفة لاستخدام Flutter في 5 دقائق

لا شكّ في أنّ مشاهدة Debug Console ممتعة بقدر ما هو مسرور، ولكنّ الزرّ هو بحاجة لتنفيذ إجراء أكثر فائدة. قبل البدء، يُرجى إلقاء نظرة فاحصة على الرمز في 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 بالكامل.

الآن، يمكنك استدعاء قائمة Refactor. في 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

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

إضافة الزر

باستخدام "منطق الأعمال" بعيدًا عن ذلك، حان الوقت للعمل على واجهة المستخدم مرة أخرى. وضع زر "الإعجاب" على يسار "التالي" يتطلب الزر Row. والتطبيق المصغّر 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.

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

حالة الضبط

لا تحتاج الأداة الجديدة ذات الحالة إلا إلى تتبُّع متغيّر واحد فقط: 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 لتحديد الشاشة التي يتم عرضها.

استخدام الفهرس المحدّد

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

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

  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".

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

  • يمكنك استخدام تطبيق "ListView" المصغّر من أجل الانتقال إلى الشاشة الرئيسية على شاشة "Column".
  • لا تنسَ أنّه عليك الوصول إلى المثيل 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.