1. مقدمة
Flutter هو مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي من قاعدة رموز برمجية واحدة. في هذا الدرس التطبيقي حول الترميز، ستنشئ تطبيق Flutter التالي:
ينشئ التطبيق أسماء لطيفة مثل "newstay" أو "lightstream" أو "mainbrake" أو "graypine". يمكن للمستخدم طلب الاسم التالي، وإضافة الاسم الحالي إلى المفضلة، ومراجعة قائمة الأسماء المفضلة في صفحة منفصلة. يستجيب التطبيق لأحجام الشاشات المختلفة.
ما ستتعرَّف عليه
- أساسيات عمل Flutter
- إنشاء التنسيقات في Flutter
- ربط تفاعلات المستخدم (مثل الضغط على الأزرار) بسلوك التطبيق
- الحفاظ على تنظيم رمز Flutter
- جعل تطبيقك متجاوبًا (للشاشات المختلفة)
- يعد تحقيق مظهر متناسق تطبيقك
ستبدأ بسقالة أساسية بحيث يمكنك الانتقال مباشرة إلى الأجزاء المثيرة للاهتمام.
سيطلعك "فيليب" على الدروس التطبيقية حول الترميز.
انقر على التالي لبدء التمرين المعملي.
2. إعداد بيئة Flutter
محرِّر
لجعل هذا الدرس التطبيقي حول الترميز بسيطًا قدر الإمكان، نفترض أنّك ستستخدم Visual Studio Code (VS Code) كبيئة التطوير. إنها مجانية وتعمل على جميع الأنظمة الأساسية الرئيسية.
لا مانع من استخدام أي محرّر تريده: "استوديو Android" أو برامج IntelliJ IDE أو 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 مع المكوّن الإضافي Flutter
- البرامج التي يتطلبها هدف التطوير الذي اخترته (مثل: Visual Studio لاستهداف نظام التشغيل Windows أو Xcode لاستهداف نظام التشغيل macOS)
في القسم التالي، عليك إنشاء مشروعك الأول على Flutter.
إذا واجهت مشاكل حتى الآن، يمكنك الاستفادة من بعض هذه الأسئلة والأجوبة (من StackOverflow) في تحديد المشاكل وحلّها.
الأسئلة الشائعة
- كيف يمكنني العثور على مسار حزمة Flutter SDK؟
- ماذا أفعل في حال عدم العثور على أمر Flutter؟
- كيف يمكنني إصلاح الخطأ "في انتظار أمر Flutter آخر لفتح قفل بدء التشغيل" المشكلة؟
- كيف يمكنني إعلام Flutter بمكان تثبيت حزمة تطوير البرامج (SDK) لنظام التشغيل Android؟
- كيف أتعامل مع خطأ Java عند تشغيل
flutter doctor --android-licenses
؟ - كيف أتعامل مع أداة Android
sdkmanager
التي لم يتم العثور عليها؟ - كيف أتعامل مع "المكوِّن
cmdline-tools
غير متوفّر" خطأ؟ - كيف يمكنني تشغيل CocoaPods على Apple Silicon (M1)؟
- كيف يمكنني إيقاف التنسيق التلقائي عند الحفظ في رمز VS؟
3- إنشاء مشروع
إنشاء مشروعك الأول على Flutter
افتح Visual Studio Code وافتح لوحة الأوامر (باستخدام F1
أو Ctrl+Shift+P
أو Shift+Cmd+P
). ابدأ كتابة "Flutter new". اختَر الأمر Flutter: مشروع جديد.
اختَر بعد ذلك التطبيق ثم اختَر المجلد الذي تريد إنشاء مشروعك فيه. قد يكون هذا دليلك الرئيسي أو شيئًا مثل C:\src\
.
أخيرًا، قم بتسمية مشروعك. شيء مثل namer_app
أو my_awesome_namer
.
ينشئ Flutter الآن مجلد المشاريع ويفتحه رمز VS.
ستعمل الآن على استبدال محتويات الملفات الثلاثة باستخدام مخزن أساسي للتطبيق.
نسخ & لصق التطبيق الأولي
في الجزء الأيمن من رمز VS، تأكَّد من اختيار 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.1.1
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
يحدِّد ملف pubspec.yaml
المعلومات الأساسية عن تطبيقك، مثل إصداره الحالي وتبعياته ومواد العرض التي سيتم شحنه بها.
بعد ذلك، افتح ملف إعداد آخر في المشروع، analysis_options.yaml
.
استبدِل محتواها بما يلي:
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، سيظهر لك زرّ يعرض الجهاز المستهدَف الحالي. انقر لتغييرها.
عندما يكون 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'),
),
],
),
);
// ...
عند حفظ التغيير، يتم تحديث التطبيق مرة أخرى: يظهر زر، وعند النقر عليه، تعرض Debug Console في رمز VS رسالة تم الضغط على زر!.
دورة مكثفة لاستخدام Flutter في 5 دقائق
لا شكّ في أنّ مشاهدة Debug Console ممتعة بقدر ما هو مسرور، ولكنّ الزرّ هو بحاجة لتنفيذ إجراء أكثر فائدة. قبل البدء، يُرجى إلقاء نظرة فاحصة على الرمز في lib/main.dart
لفهم طريقة عمله.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
في أعلى الملف، ستجد الدالة main()
. وفي شكله الحالي، يطلب من Flutter تشغيل التطبيق المحدّد في MyApp
فقط.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
وتوسِّع الفئة MyApp
نطاق StatelessWidget
. التطبيقات المصغّرة هي العناصر التي يتم من خلالها إنشاء كل تطبيق من تطبيقات Flutter. كما ترى، التطبيق نفسه هو تطبيق مصغّر.
يعمل الرمز البرمجي المتوفّر في MyApp
على إعداد التطبيق بالكامل. تنشئ الحالة على مستوى التطبيق (المزيد عن هذا لاحقًا)، وتسمي التطبيق، وتحدد المظهر المرئي، وتضبط "الصفحة الرئيسية" التطبيق المصغَّر - نقطة البداية للتطبيق.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
بعد ذلك، تحدِّد الفئة MyAppState
حالة التطبيق. هذه أول تجربة لك في استخدام Flutter، لذا سيجعل هذا الدرس التطبيقي حول الترميز بسيطًا ومركزًا. تتوفّر عدة طرق فعّالة لإدارة حالة التطبيق في Flutter. من الأسهل شرح ChangeNotifier
، وهو الأسلوب الذي يتّبعه هذا التطبيق.
- تحدِّد السمة
MyAppState
البيانات التي يحتاجها التطبيق لكي يعمل. في الوقت الحالي، يحتوي على متغيّر واحد فقط مع زوج كلمات عشوائي حالي. ستتم إضافة العناصر إلى هذه البيانات لاحقًا. - تمتد فئة الولاية إلى
ChangeNotifier
، أي أنّه يمكنها إبلاغ المستخدمين الآخرين بالتغييرات الخاصة بها. على سبيل المثال، إذا تغيّر زوج الكلمات الحالي، تحتاج بعض التطبيقات المصغّرة في التطبيق إلى معرفة ذلك. - يتم إنشاء الحالة وتقديمها إلى التطبيق بأكمله باستخدام
ChangeNotifierProvider
(يمكنك الاطّلاع على الرمز أعلاه فيMyApp
). يسمح هذا الإجراء لأي أداة في التطبيق بالاحتفاظ بالحالة.
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
بالكامل.
الآن، يمكنك استدعاء قائمة Refactor. في 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.
في بعض الأحيان، على الرغم من ذلك، تكون هناك حاجة إلى بعض العمل. في حالة هذا التطبيق، قد يواجه قارئ الشاشة مشاكل في لفظ بعض أزواج الكلمات التي تم إنشاؤها. على الرغم من أنّ البشر لا يواجهون مشاكل في تحديد الكلمتَين في كلمة رخيصة، قد يلفظ قارئ الشاشة الحرف 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>[]
، باستخدام المصطلحات العامة. ويساعد ذلك في جعل تطبيقك أكثر فعالية، وسيرفض حتى تشغيل تطبيقك إذا حاولت إضافة أي عنصر آخر غيرWordPair
إليه. في المقابل، يمكنك استخدام قائمةfavorites
لمعرفة أنّه لا يمكن أبدًا إخفاء أي عناصر غير مرغوب فيها (مثلnull
) فيها.
- لقد أضفت أيضًا طريقة جديدة،
toggleFavorite()
، والتي إما تزيل زوج الكلمات الحالي من قائمة المفضلة (إذا كانت موجودة من قبل)، أو تضيفها (إذا لم تكن موجودة بعد). وفي كلتا الحالتين، يطلب الرمزnotifyListeners();
بعد ذلك.
إضافة الزر
باستخدام "منطق الأعمال" بعيدًا عن ذلك، حان الوقت للعمل على واجهة المستخدم مرة أخرى. وضع زر "الإعجاب" على يسار "التالي" يتطلب الزر Row
. والتطبيق المصغّر Row
هو المكافئ الأفقي لـ Column
الذي رأيته في السابق.
أولاً، عليك لف الزر الحالي في Row
. انتقِل إلى طريقة build()
في MyHomePage
، وضَع المؤشر على ElevatedButton
، واستدعِ قائمة إعادة ضبط الصف باستخدام Ctrl+.
أو Cmd+.
، واختَر التفاف الصف.
عند حفظ التغييرات، ستلاحظ أنّ Row
يعمل بشكل مشابه لـ Column
، ويؤدّي إلى تحرّك العناصر الثانوية جهة اليسار بشكل تلقائي. (وضعت "Column
" أطفالها في الأعلى.) لحلّ هذه المشكلة، يمكنك استخدام الطريقة نفسها التي كانت عليها في السابق، ولكن مع "mainAxisAlignment
". ومع ذلك، يمكنك استخدام mainAxisSize
للأغراض التعليمية (التعلّم). ويؤدي ذلك إلى منع Row
من أخذ كل المساحات الأفقية المتاحة.
أجرِ التغيير التالي:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
عادت واجهة المستخدم إلى ما كانت عليه من قبل.
بعد ذلك، يمكنك إضافة الزر أعجبني وربطه بجهاز 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.
ينشئ IDE صفًا جديدًا لك، "_MyHomePageState
". تعمل هذه الفئة على توسيع نطاق State
، وبالتالي يمكنها إدارة قيمها الخاصة. (يمكنه تغيير نفسه). يُرجى العلم أيضًا أنّه تم نقل الطريقة build
من التطبيق المصغّر القديم الذي لا يتضمّن حالة إلى _MyHomePageState
(بدلاً من البقاء في التطبيق المصغّر). وتم نقله حرفيًا، ولم يحدث أي تغيير في طريقة build
. هي الآن فقط في مكان آخر.
حالة الضبط
لا تحتاج الأداة الجديدة ذات الحالة إلا إلى تتبُّع متغيّر واحد فقط: selectedIndex
. أدخِل التغييرات الثلاثة التالية على _MyHomePageState
:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
فحص التغييرات:
- عليك تقديم متغيّر جديد "
selectedIndex
" وإعداده في "0
". - يمكنك استخدام هذا المتغيّر الجديد في تعريف
NavigationRail
بدلاً من0
غير القابل للتغيير الذي كان متوفّرًا حتى الآن. - عند استدعاء الدالة
onDestinationSelected
، بدلاً من الاكتفاء بطباعة القيمة الجديدة لوحدة التحكّم، يمكنك تحديدها لـselectedIndex
داخل استدعاءsetState()
. يشبه هذا الاستدعاء طريقةnotifyListeners()
المستخدمة سابقًا، فهو يضمن تحديث واجهة المستخدم.
يستجيب شريط التنقّل الآن لتفاعل المستخدم. لكن المساحة الموسّعة على اليمين تبقى كما هي. هذا لأن الرمز لا يستخدم selectedIndex
لتحديد الشاشة التي يتم عرضها.
استخدام الفهرس المحدّد
ضَع الرمز التالي في أعلى طريقة build
الخاصة بـ _MyHomePageState
، قبل return Scaffold
مباشرةً:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
افحص هذا الجزء من التعليمة البرمجية:
- يعرّف الرمز عن متغيّر جديد،
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
. تتيح لك تغيير شجرة الأدوات بناءً على مقدار المساحة المتاحة لديك.
مرة أخرى، استخدِم قائمة Refactor من Flutter في رمز VS لإجراء التغييرات المطلوبة. ومع ذلك، فإن الأمر في هذه المرة أكثر تعقيدًا:
- داخل طريقة
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
".
في ما يلي بعض الإرشادات:
- يمكنك استخدام تطبيق "
ListView
" المصغّر من أجل الانتقال إلى الشاشة الرئيسية على شاشة "Column
". - لا تنسَ أنّه عليك الوصول إلى المثيل
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.