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
- macOS
- 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؟ - كيف يمكنني حلّ مشكلة عدم العثور على الأداة
sdkmanagerفي Android؟ - كيف يمكنني التعامل مع رسالة الخطأ "المكوّن
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هذه في الخطوة الأولى. - تأخذ أداة
TextالثانيةappState، وتصل إلى العضو الوحيد في هذه الفئة، وهو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") لتأكيد أنّك على دراية بما تفعله. (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. استخدِم السمة 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 على طول المحور الرئيسي (العمودي).

تكون العناصر الفرعية في المنتصف على طول المحور المتقاطع للعمود (أي أنّها تكون في المنتصف أفقيًا). ولكن Column نفسه ليس في وسط Scaffold. يمكننا التحقّق من ذلك باستخدام أداة فحص التطبيقات المصغّرة.
لا يتناول هذا الدرس التطبيقي حول الترميز أداة Widget Inspector نفسها، ولكن يمكنك ملاحظة أنّه عند تمييز 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()، وضَع مؤشر الماوس على 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'),
),
],
),
],
),
),
);
}
}
// ...
تعود واجهة المستخدم إلى ما كانت عليه من قبل.

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

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