1. مقدمة
Flutter هي مجموعة أدوات واجهة مستخدم من Google يمكن استخدامها لإنشاء تطبيقات للأجهزة الجوّالة والويب وأجهزة سطح المكتب من خلال قاعدة رموز برمجية واحدة. في هذا الدرس العملي، ستنشئ تطبيق Flutter التالي:
يولّد التطبيق أسماءً جذابة، مثل "newstay" أو "lightstream" أو "mainbrake" أو "graypine". يمكن للمستخدم طلب الاسم التالي، وإضافة الاسم الحالي إلى قائمة الأسماء المفضّلة، ومراجعة قائمة الأسماء المفضّلة في صفحة منفصلة. يتوافق التطبيق مع أحجام الشاشات المختلفة.
المُعطيات
- أساسيات طريقة عمل Flutter
- إنشاء تنسيقات في Flutter
- ربط تفاعلات المستخدمين (مثل الضغط على الأزرار) بسلوك التطبيق
- الحفاظ على تنظيم رمز Flutter البرمجي
- جعل تطبيقك سريع الاستجابة (للشاشات المختلفة)
- الحصول على مظهر ومضمون متسقَين لتطبيقك
ستبدأ بإطار أساسي لتتمكّن من الانتقال مباشرةً إلى الأجزاء المهمة.

في ما يلي فيديو يقدّمه "فيليب" يشرح فيه الخطوات الكاملة في هذا الدرس العملي.
انقر على "التالي" لبدء المعمل.
2. إعداد بيئة Flutter
محرِّر
لتبسيط هذا الدرس البرمجي قدر الإمكان، نفترض أنّك ستستخدم Visual Studio Code (VS Code) كبيئة تطوير. وهي مجانية وتعمل على جميع الأنظمة الأساسية الرئيسية.
بالطبع، يمكنك استخدام أي محرِّر تريده، مثل "استوديو Android" أو بيئات تطوير متكاملة أخرى من IntelliJ أو Emacs أو Vim أو Notepad++، فكلها تعمل مع Flutter.
ننصحك باستخدام VS Code لهذا الدرس التطبيقي لأنّ التعليمات تستخدم تلقائيًا اختصارات خاصة بـ VS Code. من الأسهل قول عبارات مثل "انقر هنا" أو "اضغط على هذا المفتاح" بدلاً من عبارات مثل "اتّخِذ الإجراء المناسب في المحرّر لتنفيذ X".

اختيار هدف تطوير
Flutter هي مجموعة أدوات متعددة الأنظمة الأساسية. يمكن تشغيل تطبيقك على أي من أنظمة التشغيل التالية:
- iOS
- Android
- Windows
- نظام التشغيل Mac
- Linux
- الويب
ومع ذلك، من الممارسات الشائعة اختيار نظام تشغيل واحد ستجري عليه عملية التطوير بشكل أساسي. هذا هو "هدف التطوير"، أي نظام التشغيل الذي يعمل عليه تطبيقك أثناء عملية التطوير.

على سبيل المثال، لنفترض أنّك تستخدم جهاز كمبيوتر محمول يعمل بنظام التشغيل Windows لتطوير تطبيق Flutter. إذا اخترت Android كهدف التطوير، عليك عادةً توصيل جهاز Android بجهاز الكمبيوتر المحمول الذي يعمل بنظام التشغيل Windows باستخدام كابل USB، وسيتم تشغيل التطبيق قيد التطوير على جهاز Android المتصل. ولكن يمكنك أيضًا اختيار Windows كهدف التطوير، ما يعني أنّ تطبيقك قيد التطوير يعمل كتطبيق Windows إلى جانب المحرّر.
قد يكون من المغري اختيار الويب كهدف تطوير. أما عيب هذا الخيار فهو فقدان إحدى ميزات التطوير الأكثر فائدة في Flutter، وهي Stateful Hot Reload. لا يمكن لـ Flutter إعادة التحميل السريع لتطبيقات الويب.
يُرجى تحديد اختيارك الآن. تذكَّر أنّه يمكنك دائمًا تشغيل تطبيقك على أنظمة تشغيل أخرى لاحقًا. كل ما في الأمر أنّ تحديد هدف واضح للتطوير يسهّل الخطوة التالية.
تثبيت Flutter
تتوفّر دائمًا أحدث التعليمات حول كيفية تثبيت حزمة تطوير البرامج (SDK) من Flutter على docs.flutter.dev.
لا تغطي التعليمات الواردة على موقع Flutter الإلكتروني تثبيت حزمة SDK نفسها فحسب، بل أيضًا الأدوات ذات الصلة بالهدف من التطوير والمكوّنات الإضافية للمحرّر. تذكَّر أنّه في هذا الدرس العملي، عليك تثبيت ما يلي فقط:
- حزمة تطوير البرامج (SDK) في Flutter
- محرِّر Visual Studio Code مع المكوّن الإضافي Flutter
- البرامج المطلوبة لهدف التطوير الذي اخترته (على سبيل المثال: Visual Studio لاستهداف نظام التشغيل Windows أو Xcode لاستهداف نظام التشغيل macOS)
في القسم التالي، ستنشئ مشروع Flutter الأول.
إذا واجهتك مشاكل حتى الآن، قد تجد بعض هذه الأسئلة والأجوبة (من StackOverflow) مفيدة لتحديد المشاكل وحلّها.
الأسئلة الشائعة
- كيف يمكنني العثور على مسار حزمة تطوير البرامج (SDK) الخاصة بإطار عمل Flutter؟
- ماذا أفعل عندما لا يتم العثور على أمر Flutter؟
- كيف يمكنني حلّ مشكلة "في انتظار أمر Flutter آخر لإلغاء قفل بدء التشغيل"؟
- كيف يمكنني إخبار Flutter بمكان تثبيت حزمة تطوير البرامج (SDK) لنظام التشغيل Android؟
- كيف يمكنني التعامل مع خطأ Java عند تشغيل
flutter doctor --android-licenses؟ - كيف يمكنني حلّ مشكلة عدم العثور على أداة Android
sdkmanager؟ - كيف يمكنني التعامل مع رسالة الخطأ "المكوّن
cmdline-toolsغير متوفّر"؟ - كيف يمكنني تشغيل CocoaPods على أجهزة Apple Silicon (M1)؟
- كيف يمكنني إيقاف التنسيق التلقائي عند الحفظ في VS Code؟
3- إنشاء مشروع
إنشاء مشروع Flutter الأول
شغِّل Visual Studio Code وافتح لوحة الأوامر (باستخدام F1 أو Ctrl+Shift+P أو Shift+Cmd+P). ابدأ بكتابة "flutter new". اختَر الأمر Flutter: مشروع جديد.
بعد ذلك، اختَر تطبيق ثم مجلدًا لإنشاء مشروعك فيه. يمكن أن يكون هذا المسار هو دليل المنزل أو مسارًا مثل C:\src\.
أخيرًا، امنح مشروعك اسمًا. شيء مثل namer_app أو my_awesome_namer

ينشئ Flutter الآن مجلد مشروعك ويفتحه VS Code.
ستتم الآن الكتابة فوق محتوى 3 ملفات باستخدام بنية أساسية للتطبيق.
نسخ التطبيق الأوّلي ولصقه
في اللوحة اليمنى من VS Code، تأكَّد من اختيار المستكشف (Explorer)، وافتح الملف pubspec.yaml.

استبدِل محتوى هذا الملف بما يلي:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
يحدّد ملف pubspec.yaml المعلومات الأساسية عن تطبيقك، مثل الإصدار الحالي والتبعيات والأصول التي سيتم تضمينها في الحزمة.
بعد ذلك، افتح ملف إعداد آخر في المشروع، analysis_options.yaml.

استبدِل محتواه بما يلي:
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/.

استبدِل محتوى هذا الملف بما يلي:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
تشكّل أسطر الرمز البرمجي الخمسون هذه التطبيق بأكمله حتى الآن.
في القسم التالي، شغِّل التطبيق في وضع تصحيح الأخطاء وابدأ في تطويره.
4. إضافة زر
تضيف هذه الخطوة زر التالي لإنشاء زوج كلمات جديد.
تشغيل التطبيق
أولاً، افتح lib/main.dart وتأكَّد من اختيار الجهاز المستهدَف. في أسفل يسار نافذة VS Code، ستجد زرًا يعرض الجهاز المستهدف الحالي. انقر لتغييره.
أثناء فتح lib/main.dart، ابحث عن زر "تشغيل"
في أعلى يسار نافذة VS Code وانقر عليه.
بعد دقيقة تقريبًا، يتم تشغيل تطبيقك في وضع تصحيح الأخطاء. لا يبدو أنّ هناك الكثير حتى الآن:

أول عملية إعادة تحميل سريعة
في أسفل 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. يتم تشغيل ميزة "إعادة التحميل السريع" عند حفظ التغييرات في ملف مصدر.
الأسئلة الشائعة
- ماذا لو لم تعمل ميزة "إعادة التحميل السريع" في VSCode؟
- هل عليّ الضغط على "r" لإعادة التحميل السريع في VSCode؟
- هل تعمل ميزة "إعادة التحميل السريع" على الويب؟
- كيف يمكنني إزالة بانر "تصحيح الأخطاء"؟
إضافة زر
بعد ذلك، أضِف زرًا في أسفل Column، أسفل مثيل Text الثاني مباشرةً.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
عند حفظ التغيير، يتم تحديث التطبيق مرة أخرى: يظهر زر، وعند النقر عليه، تعرض وحدة تصحيح الأخطاء في VS Code الرسالة تم النقر على الزر!.
دورة مكثّفة عن Flutter في 5 دقائق
على الرغم من أنّ مشاهدة وحدة التحكّم في تصحيح الأخطاء ممتعة، إلا أنّك تريد أن يؤدي الزر وظيفة أكثر أهمية. قبل الانتقال إلى ذلك، ألقِ نظرة فاحصة على الرمز في lib/main.dart لفهم طريقة عمله.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
في أعلى الملف، ستجد الدالة main(). في شكله الحالي، يطلب من Flutter تشغيل التطبيق المحدّد في MyApp فقط.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
تمتد الفئة MyApp إلى StatelessWidget. التطبيقات المصغّرة هي العناصر التي يمكنك من خلالها إنشاء كل تطبيقات Flutter. وكما ترى، حتى التطبيق نفسه هو تطبيق مصغّر.
يعمل الرمز البرمجي في MyApp على إعداد التطبيق بأكمله، إذ ينشئ حالة التطبيق بأكمله (سنتحدّث عن ذلك لاحقًا)، ويسمّي التطبيق، ويحدّد المظهر المرئي، ويضبط أداة "الصفحة الرئيسية"، أي نقطة البداية لتطبيقك.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
بعد ذلك، تحدّد الفئة MyAppState حالة التطبيق. هذه هي تجربتك الأولى مع Flutter، لذا سنبقي هذا الدرس البرمجي بسيطًا ومركّزًا. تتوفّر العديد من الطرق الفعّالة لإدارة حالة التطبيق في Flutter. إحدى أسهل الطرق التي يمكن شرحها هي ChangeNotifier، وهي الطريقة التي يتّبعها هذا التطبيق.
- تحدّد
MyAppStateالبيانات التي يحتاجها التطبيق ليعمل. في الوقت الحالي، يحتوي على متغيّر واحد فقط يتضمّن زوج الكلمات العشوائي الحالي. ستضيف إلى هذا لاحقًا. - توسّع فئة الحالة
ChangeNotifier، ما يعني أنّه يمكنها إبلاغ الآخرين بالتغييرات التي تطرأ عليها. على سبيل المثال، إذا تغيّر زوج الكلمات الحالي، تحتاج بعض التطبيقات المصغّرة في التطبيق إلى معرفة ذلك. - يتم إنشاء الحالة وتوفيرها للتطبيق بأكمله باستخدام
ChangeNotifierProvider(راجِع الرمز أعلاه فيMyApp). يتيح ذلك لأي أداة في التطبيق الحصول على الحالة.

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، وهي الأداة التي عدّلتها من قبل. يتوافق كل سطر مرقّم أدناه مع تعليق رقم السطر في الرمز أعلاه:
- يحدّد كل عنصر واجهة مستخدم طريقة
build()يتم استدعاؤها تلقائيًا في كل مرة تتغير فيها ظروف عنصر واجهة المستخدم، وذلك لضمان أن يكون عنصر واجهة المستخدم محدّثًا دائمًا. - تتتبّع
MyHomePageالتغييرات التي تطرأ على الحالة الحالية للتطبيق باستخدام الطريقةwatch. - يجب أن تعرض كل طريقة
buildتطبيقًا مصغّرًا أو (عادةً) شجرة متداخلة من التطبيقات المصغّرة. في هذه الحالة، يكون التطبيق المصغّر ذو المستوى الأعلى هوScaffold. لن تعمل معScaffoldفي هذا الدرس البرمجي، ولكنّه أداة مفيدة ويمكن العثور عليها في الغالبية العظمى من تطبيقات Flutter الواقعية. -
Columnهي إحدى أبسط أدوات تخطيط واجهة المستخدم في Flutter. تأخذ أي عدد من العناصر الفرعية وتضعها في عمود من أعلى إلى أسفل. تضع هذه السمة تلقائيًا العناصر الثانوية في أعلى العمود. ستغيّر هذا الإعداد قريبًا ليتم توسيط العمود. - لقد غيّرت أداة
Textهذه في الخطوة الأولى. - يأخذ التطبيق المصغّر الثاني
TextappState، ويصل إلى العضو الوحيد في هذه الفئة، وهوcurrent(وهوWordPair). يوفّرWordPairالعديد من دوال الجلب المفيدة، مثلasPascalCaseأوasSnakeCase. نستخدم هناasLowerCase، ولكن يمكنك تغيير ذلك الآن إذا كنت تفضّل أحد البدائل. - لاحظ كيف أنّ رمز 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- تحسين مظهر التطبيق
هذا هو شكل التطبيق في الوقت الحالي.

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

استخراج تطبيق مصغّر
يبدو السطر المسؤول عن عرض زوج الكلمات الحالي على النحو التالي الآن: 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، يمكنك إجراء ذلك بإحدى الطريقتَين التاليتَين:
- انقر بزر الماوس الأيمن على جزء الرمز الذي تريد إعادة تركيبه (
Textفي هذه الحالة) واختَر إعادة التركيب... من القائمة المنسدلة.
أو
- حرِّك المؤشر إلى رمز الجزء الذي تريد إعادة تصنيعه (
Textفي هذه الحالة)، واضغط علىCtrl+.(في نظام التشغيل Windows أو Linux) أوCmd+.(في نظام التشغيل Mac).
في قائمة إعادة البناء، اختَر استخراج أداة. عيِّن اسمًا، مثل BigCard، وانقر على Enter.
يؤدي ذلك إلى إنشاء صف جديد تلقائيًا، BigCard، في نهاية الملف الحالي. يبدو الصف على النحو التالي:
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
لاحظ كيف يواصل التطبيق العمل حتى أثناء إعادة البناء هذه.
إضافة بطاقة
حان الوقت الآن لتحويل هذه الأداة الجديدة إلى عنصر واجهة مستخدم بارز كما تخيّلنا في بداية هذا القسم.
ابحث عن الفئة BigCard والطريقة build() بداخلها. كما كان من قبل، استدعِ قائمة إعادة تصميم الرمز في الأداة Text. ومع ذلك، لن تستخرج التطبيق المصغّر هذه المرة.
بدلاً من ذلك، اختَر التفاف مع ترك مساحة فارغة. يؤدي ذلك إلى إنشاء أداة رئيسية جديدة حول الأداة Text باسم Padding. بعد الحفظ، ستلاحظ أنّ الكلمة العشوائية أصبحت تتضمّن مساحة أكبر.
زيادة مساحة الحشو عن القيمة التلقائية 8.0 على سبيل المثال، استخدِم شيئًا مثل 20 للحصول على مساحة متروكة أكبر.
بعد ذلك، انتقِل إلى مستوى أعلى. ضَع مؤشر الماوس على التطبيق المصغّر Padding، وافتح قائمة إعادة تصميم الرمز، ثم اختَر تضمين التطبيق المصغّر....
يتيح لك ذلك تحديد الأداة الرئيسية. اكتب "بطاقة" واضغط على Enter.
يغلّف هذا الرمز أداة Padding، وبالتالي يغلّف أيضًا Text، باستخدام أداة Card.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
سيظهر التطبيق الآن على النحو التالي:

المظهر والأسلوب
لجعل البطاقة أكثر تميزًا، يمكنك تلوينها بلون أكثر ثراءً. ولأنّه من الجيد دائمًا الحفاظ على نظام ألوان متّسق، استخدِم 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هو اللون الأبرز الذي يحدّد لون التطبيق.
تم الآن تلوين البطاقة باللون الأساسي للتطبيق:

يمكنك تغيير هذا اللون ونظام الألوان للتطبيق بأكمله من خلال الانتقال للأعلى إلى 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لنظام الألوان لونًا مناسبًا للاستخدام على اللون الأساسي للتطبيق.
من المفترض أن يظهر التطبيق الآن على النحو التالي:

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

ومع ذلك، قد تحتاج أحيانًا إلى إجراء بعض التعديلات. في حالة هذا التطبيق، قد يواجه قارئ الشاشة مشاكل في نطق بعض أزواج الكلمات التي تم إنشاؤها. في حين لا يواجه البشر مشاكل في تحديد الكلمتَين في cheaphead، قد يلفظ قارئ الشاشة ph في منتصف الكلمة على النحو f.
الحلّ هو استبدال pair.asLowerCase بـ "${pair.first} ${pair.second}". يستخدم الأخير عملية استيفاء السلسلة لإنشاء سلسلة (مثل "cheap head") من الكلمتَين الواردتَين في pair. يضمن استخدام كلمتين منفصلتين بدلاً من كلمة مركّبة أن تتعرّف برامج قراءة الشاشة عليهما بشكل صحيح، كما يوفّر تجربة أفضل للمستخدمين الذين يعانون ضعفًا في النظر.
ومع ذلك، قد تحتاج إلى الحفاظ على البساطة المرئية في pair.asLowerCase. استخدِم السمة Text مع الخاصية semanticsLabel لتجاوز المحتوى المرئي لعنصر واجهة المستخدم النصي بمحتوى دلالي أكثر ملاءمة لأجهزة قراءة الشاشة:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
الآن، تنطق برامج قراءة الشاشة كل زوج من الكلمات التي تم إنشاؤها بشكل صحيح، ولكن تظل واجهة المستخدم كما هي. جرِّب ذلك عمليًا من خلال استخدام قارئ شاشة على جهازك.
توسيط واجهة المستخدم
بعد أن تم عرض زوج الكلمات العشوائي مع قدر كافٍ من الجاذبية البصرية، حان الوقت لوضعه في وسط نافذة/شاشة التطبيق.
أولاً، تذكَّر أنّ BigCard هو جزء من Column. تجمع الأعمدة تلقائيًا العناصر الفرعية في الأعلى، ولكن يمكننا إلغاء هذا الإعداد. انتقِل إلى طريقة MyHomePage في build() وأجرِ التغيير التالي:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
يؤدي ذلك إلى توسيط العناصر الثانوية داخل Column على طول المحور الرئيسي (العمودي).

تكون العناصر الفرعية في المنتصف على طول المحور المتقاطع للعمود (أي أنّها تكون في المنتصف أفقيًا). ولكن Column نفسه ليس في وسط Scaffold. يمكننا التحقّق من ذلك باستخدام أداة فحص التطبيقات المصغّرة.
لا يندرج "فاحص التطبيقات المصغّرة" نفسه ضمن نطاق هذا الدرس العملي، ولكن يمكنك ملاحظة أنّه عند تمييز Column، لا يشغل العرض الكامل للتطبيق، بل يشغل فقط المساحة الأفقية التي تحتاج إليها العناصر التابعة له.
يمكنك توسيط العمود نفسه. ضَع مؤشر الماوس على Column، وافتح قائمة إعادة التركيب (باستخدام Ctrl+. أو Cmd+.)، ثم اختَر التضمين في Center.
من المفترض أن يظهر التطبيق الآن على النحو التالي:

يمكنك تعديل هذا الإعداد أكثر إذا أردت.
- يمكنك إزالة أداة
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'),
),
],
),
),
);
}
}
// ...
ويظهر التطبيق على النحو التالي:

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

إضافة منطق النشاط التجاري
انتقِل إلى MyAppState وأضِف الرمز التالي:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
فحص التغييرات:
- أضفت سمة جديدة إلى
MyAppStateباسمfavorites. يتم ضبط قيمة هذه السمة على قائمة فارغة:[]. - حدّدت أيضًا أنّه لا يمكن أن تحتوي القائمة إلا على أزواج من الكلمات:
<WordPair>[]، باستخدام الأنواع العامة. يساعد ذلك في جعل تطبيقك أكثر قوة، إذ يرفض Dart حتى تشغيل تطبيقك إذا حاولت إضافة أي شيء آخر غيرWordPairإليه. وبالتالي، يمكنك استخدام قائمةfavoritesمع العلم أنّه لا يمكن أن تكون هناك أي عناصر غير مرغوب فيها (مثلnull) مخفية فيها.
- أضفت أيضًا طريقة جديدة،
toggleFavorite()، تزيل زوج الكلمات الحالي من قائمة الكلمات المفضّلة (إذا كان موجودًا فيها)، أو تضيفه (إذا لم يكن موجودًا بعد). في كلتا الحالتين، يستدعي الرمزnotifyListeners();بعد ذلك.
إضافة الزر
بعد الانتهاء من "منطق النشاط التجاري"، حان الوقت للعمل على واجهة المستخدم مرة أخرى. يتطلّب وضع زرّ "أعجبني" على يسار زرّ "التالي" Row. الأداة Row هي المكافئ الأفقي للأداة Column التي رأيتها سابقًا.
أولاً، ضع الزر الحالي في علامة Row. انتقِل إلى طريقة MyHomePage، وضَع مؤشر الماوس على build()، وافتح قائمة إعادة التركيب باستخدام Ctrl+. أو Cmd+.، ثم اختَر التضمين في صف.ElevatedButton
عند الحفظ، ستلاحظ أنّ Row يتصرف بشكل مشابه لـ Column، إذ يجمع العناصر الفرعية إلى اليمين تلقائيًا. (Column جمعت العناصر الفرعية في الأعلى). لحلّ هذه المشكلة، يمكنك اتّباع النهج نفسه كما في السابق، ولكن مع إضافة mainAxisAlignment. ومع ذلك، لأغراض تعليمية، استخدِم mainAxisSize. يخبر هذا الإعداد Row بعدم استخدام كل المساحة الأفقية المتاحة.
أجرِ التغيير التالي:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
تعود واجهة المستخدم إلى ما كانت عليه من قبل.

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

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

في ما يلي إحدى طرق إضافة الزر الثاني إلى 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.

للوصول إلى جوهر هذه الخطوة في أقرب وقت ممكن، قسِّم 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'),
),
],
),
],
),
);
}
}
// ...
عند الحفظ، ستلاحظ أنّ الجانب المرئي من واجهة المستخدم جاهز، ولكنّه لا يعمل. لا يؤدي النقر على رمز ♥︎ (القلب) في شريط التنقّل إلى أي إجراء.

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

- يقسّم عنصرَا
Expandedواجهة المستخدم كل المساحة الأفقية المتاحة بينهما، على الرغم من أنّ شريط التنقّل يحتاج فقط إلى جزء صغير على اليمين. - داخل الأداة
Expanded، هناكContainerملون، وداخل الحاوية، هناكGeneratorPage.
التطبيقات المصغّرة التي لا تعتمد على الحالة مقابل التطبيقات المصغّرة التي تعتمد على الحالة
حتى الآن، كان تطبيق MyAppState يلبّي جميع احتياجاتك في ولايتك. لهذا السبب، تكون جميع التطبيقات المصغّرة التي كتبتها حتى الآن بلا حالة. ولا تحتوي على أي حالة قابلة للتغيير خاصة بها. لا يمكن لأي من التطبيقات المصغّرة تغيير نفسها، بل يجب أن تمرّ عبر MyAppState.
لكنّ هذا الأمر سيتغيّر قريبًا.
تحتاج إلى طريقة للاحتفاظ بقيمة selectedIndex في شريط التنقّل. يجب أن تتمكّن أيضًا من تغيير هذه القيمة من داخل دالة الاستدعاء onDestinationSelected.
يمكنك إضافة selectedIndex كسمة أخرى من سمات MyAppState. وكانت تنجح. ولكن يمكنك أن تتخيل أنّ حالة التطبيق ستتجاوز الحدّ المعقول بسرعة إذا كان كل تطبيق مصغّر يخزّن قيمه فيها.

بعض الحالات ذات صلة بأداة واحدة فقط، لذا يجب أن تبقى مع هذه الأداة.
أدخِل 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(),
),
),
],
),
);
}
}
// ...
فحص التغييرات:
- يمكنك تقديم متغيّر جديد،
selectedIndex، وتعيين قيمته الأولية إلى0. - يمكنك استخدام هذا المتغيّر الجديد في تعريف
NavigationRailبدلاً من0المرمّز ثابتًا الذي كان متوفّرًا حتى الآن. - عند استدعاء دالة معاودة الاتصال
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');
}
// ...
اطّلِع على جزء الرمز البرمجي التالي:
- يعرّف الرمز متغيرًا جديدًا،
page، من النوعWidget. - بعد ذلك، تحدّد عبارة switch شاشة لعنصر
page، وفقًا للقيمة الحالية فيselectedIndex. - بما أنّه لا يتوفّر
FavoritesPageبعد، استخدِمPlaceholder، وهو تطبيق مصغّر مفيد يرسم مستطيلاً متقاطعًا في أي مكان تضعه فيه، ما يشير إلى أنّ هذا الجزء من واجهة المستخدم لم يكتمل بعد.

- من خلال تطبيق مبدأ الفشل السريع، يحرص بيان switch أيضًا على عرض خطأ إذا لم تكن قيمة
selectedIndexهي 0 أو 1. يساعد ذلك في منع حدوث أخطاء في المستقبل. إذا أضفت وجهة جديدة إلى شريط التنقّل ونسيت تعديل هذا الرمز، سيتعطّل البرنامج أثناء التطوير (بدلاً من أن يتركك تخمّن سبب عدم عمل الأشياء أو يسمح لك بنشر رمز يتضمّن أخطاء في مرحلة الإنتاج).
بعد أن أصبح page يحتوي على الأداة التي تريد عرضها على اليسار، يمكنك على الأرجح تخمين التغيير الآخر المطلوب.
في ما يلي قيمة _MyHomePageState بعد إجراء هذا التغيير المتبقي:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
ينتقل التطبيق الآن بين GeneratorPage والعنصر النائب الذي سيصبح قريبًا صفحة المفضّلة.
سرعة الاستجابة
بعد ذلك، اجعل شريط التنقّل سريع الاستجابة. أي اجعلها تعرض التصنيفات تلقائيًا (باستخدام extended: true) عندما تتوفّر مساحة كافية لها.

توفّر Flutter العديد من الأدوات التي تساعدك في جعل تطبيقاتك متجاوبة تلقائيًا. على سبيل المثال، Wrap هي أداة مشابهة للأداة Row أو Column تعمل تلقائيًا على نقل العناصر الثانوية إلى "السطر" التالي (يُسمى "تشغيل") عندما لا تتوفّر مساحة عمودية أو أفقية كافية. هناك FittedBox، وهي أداة تتناسب تلقائيًا مع المساحة المتوفّرة وفقًا لمواصفاتك.
لكنّ NavigationRail لا يعرض التصنيفات تلقائيًا عندما تكون هناك مساحة كافية لأنّه لا يمكنه معرفة ما يعتبر مساحة كافية في كل سياق. ويعود إليك، أي المطوّر، اتّخاذ هذا القرار.
لنفترض أنّك قرّرت عرض التصنيفات فقط إذا كان عرض MyHomePage يبلغ 600 بكسل على الأقل.
التطبيق المصغّر الذي سيتم استخدامه في هذه الحالة هو LayoutBuilder. تتيح لك هذه السمة تغيير بنية التطبيق المصغّر استنادًا إلى المساحة المتوفرة لديك.
مرّة أخرى، استخدِم قائمة إعادة تصميم في Flutter في VS Code لإجراء التغييرات المطلوبة. في هذه المرة، الأمر أكثر تعقيدًا بعض الشيء:
- داخل طريقة
buildالخاصة بـ_MyHomePageState، ضَع مؤشر الماوس علىScaffold. - استدعِ قائمة إعادة الهيكلة (Refactor) باستخدام
Ctrl+.(في نظام التشغيل Windows أو Linux) أو Cmd+.(في نظام التشغيل Mac). - اختَر التضمين في أداة الإنشاء واضغط على Enter.
- عدِّل اسم
Builderالذي تمت إضافته مؤخرًا إلىLayoutBuilder. - عدِّل قائمة مَعلمات دالة الرجوع من
(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يحتوي على قائمة سلاسل، يمكنك استخدام رمز برمجي مثل ما يلي:

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

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

المواضيع التي تناولناها
- أساسيات طريقة عمل Flutter
- إنشاء تنسيقات في Flutter
- ربط تفاعلات المستخدمين (مثل الضغط على الأزرار) بسلوك التطبيق
- الحفاظ على تنظيم رمز Flutter البرمجي
- جعل تطبيقك متجاوبًا
- الحصول على مظهر ومضمون متسقَين لتطبيقك
ما هي الخطوات التالية؟
- جرِّب المزيد من الميزات في التطبيق الذي كتبته خلال هذا التدريب العملي.
- اطّلِع على الرمز البرمجي لهذه النسخة المتقدّمة من التطبيق نفسه لمعرفة كيفية إضافة قوائم متحركة وتدرّجات لونية وتأثيرات تلاشي تدريجي وغير ذلك.
- يمكنك متابعة رحلة التعلّم من خلال الانتقال إلى flutter.dev/learn.