1. مقدمة
Flutter هي مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي من خلال قاعدة رموز برمجية واحدة. في هذا الدليل التعليمي حول الرموز البرمجية، ستُنشئ تطبيق Flutter التالي:
ينشئ التطبيق أسماءً رائعة، مثل "newstay" أو "lightstream" أو "mainbrake" أو "graypine". يمكن للمستخدم طلب الاسم التالي، وإضافته إلى المفضّلة، ومراجعة قائمة الأسماء المفضّلة في صفحة منفصلة. أن يكون التطبيق متوافقًا مع أحجام الشاشات المختلفة
المُعطيات
- أساسيات آلية عمل Flutter
- إنشاء تنسيقات في Flutter
- ربط تفاعلات المستخدِمين (مثل الضغط على الأزرار) بسلوك التطبيق
- الحفاظ على تنظيم رمز Flutter البرمجي
- جعل تطبيقك سريع الاستجابة (للشاشات المختلفة)
- تحقيق مظهر ومضمون متسقَين لتطبيقك
ستبدأ بإطار عمل أساسي حتى تتمكّن من الانتقال مباشرةً إلى الأجزاء المثيرة للاهتمام.
في ما يلي فيديو يقدّمه فيليب يشرح فيه خطوات إنشاء المشروع بالكامل.
انقر على "التالي" لبدء التجربة.
2. إعداد بيئة Flutter
محرِّر
لتسهيل استخدام هذا الدليل التعليمي حول الرموز البرمجية قدر الإمكان، نفترض أنّك ستستخدم Visual Studio Code (VS Code) كبيئة تطوير. وهو متاح مجانًا ويعمل على جميع الأنظمة الأساسية الرئيسية.
يمكنك بالطبع استخدام أي محرِّر تريده: Android Studio أو أدوات تطوير البرامج المتكاملة الأخرى من 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، وهي ميزة "إعادة التحميل السريع" التي تتضمّن حالة التطبيق. لا يمكن إعادة تحميل تطبيقات الويب بسرعة باستخدام Flutter.
حدِّد اختيارك الآن. تذكَّر أنّه يمكنك دائمًا تشغيل تطبيقك على أنظمة تشغيل أخرى لاحقًا. إنّ تحديد هدف تطوير واضح يجعل الخطوة التالية أكثر سلاسة.
تثبيت Flutter
يمكنك العثور على أحدث التعليمات حول كيفية تثبيت حزمة Flutter SDK على docs.flutter.dev في أي وقت.
لا تتناول التعليمات على موقع Flutter الإلكتروني عملية تثبيت حزمة SDK نفسها فحسب، بل تتناول أيضًا الأدوات المتعلّقة بهدف التطوير ومكونات إضافية للمحرِّر. تذكَّر أنّه في هذا الدليل التعليمي حول رموز البرامج، ما عليك سوى تثبيت ما يلي:
- حزمة تطوير البرامج (SDK) من Flutter
- Visual Studio Code مع المكوّن الإضافي Flutter
- البرنامج المطلوب لاستهداف التطوير الذي اخترته (على سبيل المثال، Visual Studio لاستهداف نظام التشغيل Windows أو Xcode لاستهداف نظام التشغيل macOS)
في القسم التالي، ستنشئ مشروعك الأول باستخدام Flutter.
إذا واجهت مشاكل إلى الآن، قد تكون بعض هذه الأسئلة والإجابات (من StackOverflow) مفيدة لتحديد المشاكل وحلّها.
الأسئلة الشائعة
- كيف يمكنني العثور على مسار حزمة تطوير البرامج (SDK) الخاصة بمنصّة Flutter؟
- ماذا أفعل في حال عدم العثور على أمر Flutter؟
- كيف يمكنني حلّ مشكلة "الانتظار لتلقّي أمر آخر من flutter لإزالة قفل بدء التشغيل"؟
- كيف يمكنني إخبار Flutter بمكان تثبيت حزمة Android SDK؟
- كيف يمكنني التعامل مع خطأ 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: مشروع جديد (Flutter: New Project).
بعد ذلك، اختَر التطبيق ثم مجلدًا لإنشاء مشروعك فيه. قد يكون هذا الدليل هو الدليل الرئيسي أو رمز مثل 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' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: ^3.6.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
يحدِّد ملف pubspec.yaml
معلومات أساسية عن تطبيقك، مثل إصداره الحالي وعناصر الاعتماد فيه وملفات الأصول التي سيتم شحنه بها.
بعد ذلك، افتح ملف إعدادات آخر في المشروع analysis_options.yaml
.
استبدِل محتوياتها بما يلي:
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(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [
Text('A random idea:'),
Text(appState.current.asLowerCase),
],
),
);
}
}
تشكّل هذه السطور الخمسة والخمسون من الرمز البرمجي التطبيق بأكمله حتى الآن.
في القسم التالي، يمكنك تشغيل التطبيق في وضع تصحيح الأخطاء وبدء عملية التطوير.
4. إضافة زر
تضيف هذه الخطوة زر التالي لإنشاء مطابقة كلمات جديدة.
افتح التطبيق.
أولاً، افتح lib/main.dart
وتأكَّد من اختيار جهازك المستهدَف. في أسفل يسار VS Code، سيظهر لك زر يعرض الجهاز المستهدَف الحالي. انقر على الإعداد لتغييره.
عندما يكون lib/main.dart
مفتوحًا، ابحث عن زر "تشغيل" في أعلى يسار نافذة 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(
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
). يتيح ذلك لأي تطبيق مصغّر في التطبيق الحصول على الحالة.
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+.
(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
.
المظهر والأسلوب
لجعل البطاقة أكثر بروزًا، يمكنك طلاءها بلون أكثر ثراءً. ولأنّه من الجيد دائمًا الحفاظ على نظام ألوان متسق، استخدِم 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
(Win/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
. يمكننا التحقّق من ذلك باستخدام أداة فحص التطبيقات المصغّرة.
لا يدخل "أداة فحص التطبيقات المصغّرة" نفسها في نطاق هذا الدليل التعليمي حول رموز البرامج، ولكن يمكنك ملاحظة أنّه عند تمييز Column
، لا يشغل العنصر عرض التطبيق بالكامل، بل يشغل فقط المساحة الأفقية التي يحتاجها العنصران الفرعان.
يمكنك ببساطة وضع العمود في المنتصف. ضَع مؤشر الماوس على Column
، ثم افتح قائمة إعادة التشكيل (باستخدام Ctrl+.
أو Cmd+.
)، واختَر اللفّ باستخدام المركز.
من المفترض أن يظهر التطبيق الآن على النحو التالي:
يمكنك تعديل هذه الإعدادات أكثر إذا أردت.
- يمكنك إزالة التطبيق المصغّر
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
. Widget Row
هو المكافئ الأفقي لـ Column
الذي رأيته سابقًا.
أولاً، احط الزرّ الحالي بعنصر Row
. انتقِل إلى طريقة build()
في MyHomePage
، واضبط مؤشر الماوس على ElevatedButton
، ثم افتح قائمة إعادة التشكيل باستخدام Ctrl+.
أو Cmd+.
، واختَر التفاف بصف.
عند الحفظ، ستلاحظ أنّ Row
يعمل بشكل مشابه لـ Column
، حيث يجمع عناصره الفرعية تلقائيًا على يمين الصفحة. (Column
جمعت العناصر الفرعية في أعلى الصفحة). لحلّ هذه المشكلة، يمكنك اتّباع النهج نفسه المستخدَم سابقًا، ولكن مع استخدام mainAxisAlignment
. ومع ذلك، لأغراض تعليمية، استخدِم mainAxisSize
. يطلب هذا الإجراء من Row
عدم استخدام كل المساحة الأفقية المتاحة.
أجرِ التغيير التالي:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
عادت واجهة المستخدم إلى وضعها السابق.
بعد ذلك، أضِف زر أعجبني واربطه بالعنصر 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
. - بعد ذلك، تُحدِّد عبارة التبديل شاشة لـ
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
. ويتيح لك تغيير شجرة التطبيقات المصغّرة حسب المساحة المتاحة لديك.
استخدِم مرة أخرى قائمة إعادة التشكيل في Flutter في VS Code لإجراء التغييرات المطلوبة. هذه المرة، الأمر أكثر تعقيدًا:
- ضَع مؤشر الماوس على
Scaffold
داخل طريقةbuild
في_MyHomePageState
. - افتح قائمة إعادة التحليل باستخدام
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
. وفويلا!
يمكنك الحصول على الرمز البرمجي النهائي لهذا التطبيق في مستودع codelab على GitHub.
9. الخطوات التالية
تهانينا!
رائعة. لقد أخذت إطار عمل غير وظيفي يتضمّن أداة Column
ووحدتَي Text
، وحوّلته إلى تطبيق صغير وسريع الاستجابة وممتع.
المواضيع التي تناولناها
- أساسيات آلية عمل Flutter
- إنشاء تنسيقات في Flutter
- ربط تفاعلات المستخدِمين (مثل الضغط على الأزرار) بسلوك التطبيق
- الحفاظ على تنظيم رمز Flutter البرمجي
- إتاحة تطبيقك على الأجهزة الجوّالة
- تحقيق مظهر ومضمون متسقَين لتطبيقك
ماذا بعد ذلك؟
- يمكنك إجراء المزيد من التجارب على التطبيق الذي كتبته خلال هذا البرنامج التدريبي.
- اطّلِع على رمز هذا الإصدار المتقدّم من التطبيق نفسه لمعرفة كيفية إضافة قوائم متحركة وتدرّجات وتأثيرات تمويهية وغيرها.
- يمكنك متابعة رحلة التعلّم من خلال الانتقال إلى flutter.dev/learn.