1. מבוא
Flutter היא ערכת הכלים לבניית ממשק משתמש של Google, שנועדה לבנות אפליקציות לנייד, לאינטרנט ולמחשב על בסיס קוד יחיד. ב-Codelab הזה, תפתחו את אפליקציית Flutter הבאה:
האפליקציה יוצרת שמות שנשמעים קרירים כמו newstay, lightstream, mainbrake או graypine. המשתמש יכול לבקש את השם הבא, להוסיף את השם הנוכחי למועדפים ולבדוק את רשימת השמות המועדפים בדף נפרד. האפליקציה מגיבה לגדלים שונים של מסכים.
מה תלמדו
- העקרונות הבסיסיים של Flutter
- יצירת פריסות ב-Flutter
- קישור בין אינטראקציות של משתמשים (כמו לחיצות על לחצנים) להתנהגות באפליקציה
- שמירה על ארגון הקוד של Flutter
- איך להפוך את האפליקציה לרספונסיבית (למסכים שונים)
- השגת מראה עקבי של האפליקציה
תתחילו עם פיסול בסיסי כדי שתוכלו לקפוץ ישירות לחלקים המעניינים.
והנה פיליפ שידריך אתכם בכל ה-Codelab!
לוחצים על 'הבא' כדי להתחיל את שיעור ה-Lab.
2. הגדרת הסביבה של Flutter
עריכה
כדי שה-Codelab הזה יהיה פשוט ככל האפשר, אנחנו מניחים שתשתמשו ב-Visual Studio Code (VS Code) כסביבת הפיתוח שלכם. השירות ניתן בחינם ופועל בכל הפלטפורמות העיקריות.
כמובן שאין בעיה להשתמש בכל כלי עריכה שרוצים: Android Studio, סביבות פיתוח משולבות (IDE) אחרות של IntelliJ, Emocs, Vim או Notepad++. כולם עובדים עם Flutter.
מומלץ להשתמש ב-VS Code עבור ה-Codelab הזה כי כברירת מחדל ההוראות הן קיצורי דרך ספציפיים ל-VS Code. קל יותר לומר דברים כמו "click here" או "מקישים על המקש הזה" במקום משהו כמו "ביצוע הפעולה המתאימה בעורך כדי לבצע X".
בחירת יעד פיתוח
Flutter היא ערכת כלים מרובת פלטפורמות. האפליקציה יכולה לפעול בכל אחת ממערכות ההפעלה הבאות:
- iOS
- Android
- Windows
- macOS
- Linux
- אינטרנט
עם זאת, מקובל לבחור מערכת הפעלה אחת שתפתחו בעיקר. זהו 'יעד הפיתוח' שלכם – מערכת ההפעלה שבה האפליקציה פועלת במהלך הפיתוח.
לדוגמה, נניח שאתם משתמשים במחשב נייד עם Windows כדי לפתח אפליקציית Flutter. אם בוחרים ב-Android כיעד הפיתוח, בדרך כלל מחברים מכשיר Android למחשב הנייד עם Windows באמצעות כבל USB, והאפליקציה בשלב הפיתוח פועלת במכשיר Android המחובר הזה. אבל אפשר גם לבחור ב-Windows כיעד הפיתוח, כלומר האפליקציה בשלב הפיתוח פועלת כאפליקציה ל-Windows לצד העורך.
מפתה לבחור את האינטרנט כיעד הפיתוח שלכם. החיסרון של הבחירה הזו הוא אובדן של אחת מתכונות הפיתוח השימושיות ביותר של Flutter: 'טעינה חמה של מצב' (Stateful Reload). Flutter לא יכולה לטעון מחדש אפליקציות אינטרנט במהירות.
יש לך אפשרות לבחור עכשיו. חשוב לזכור: תמיד אפשר להריץ את האפליקציה במערכות הפעלה אחרות בשלב מאוחר יותר. אם תקפידו על יעד פיתוח ברור, השלב הבא יהיה חלק יותר.
להתקנת Flutter
ההוראות העדכניות ביותר להתקנה של Flutter SDK תמיד זמינות בכתובת docs.flutter.dev.
ההוראות באתר Flutter כוללות לא רק את ההתקנה של ה-SDK עצמה, אלא גם את הכלים הקשורים ליעד הפיתוח ואת יישומי הפלאגין של העריכה. חשוב לזכור שב-Codelab הזה צריך להתקין רק את הדברים הבאים:
- SDK של Flutter
- Visual Studio Code עם הפלאגין Flutter
- התוכנה הנדרשת על ידי יעד הפיתוח שבחרתם (לדוגמה: Visual Studio כדי לטרגט ל-Windows, או Xcode כדי לטרגט macOS)
בקטע הבא תיצרו את פרויקט Flutter הראשון שלכם.
אם נתקלתם בבעיות עד עכשיו, יכול להיות שחלק מהשאלות והתשובות האלה (מ-StackOverflow) יעזרו לכם לפתור את הבעיה.
שאלות נפוצות
- איך מוצאים את הנתיב של Flutter SDK?
- מה עושים אם הפקודה 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 Project.
בשלב הבא בוחרים ב-Application ואז בתיקייה שבה תיצרו את הפרויקט. זו יכולה להיות ספריית הבית שלך, או משהו כמו C:\src\
.
לסיום, נותנים שם לפרויקט. משהו כמו namer_app
או my_awesome_namer
.
עכשיו Flutter יוצרת את תיקיית הפרויקט שלך ו-VS Code פותח אותה.
עכשיו התוכן של שלושה קבצים יוחלף בפיגום בסיסי של האפליקציה.
העתקה & הדבקת האפליקציה הראשונית
בחלונית השמאלית של 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.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),
],
),
);
}
}
50 שורות הקוד האלה הן כל התוכן של האפליקציה עד כה.
בקטע הבא, מריצים את האפליקציה במצב ניפוי באגים ומתחילים לפתח.
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. טעינה מחדש חמה מופעלת כששומרים שינויים בקובץ מקור.
שאלות נפוצות
- מה אם לא ניתן להשתמש ב-Hot Reload ב-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
מגדירה את מצב האפליקציה...well... זו ההתנסות הראשונה שלכם ב-Flutter, כך שה-Codelab הזה יהיה פשוט וממוקד. יש הרבה דרכים מתקדמות לניהול מצב האפליקציה ב-Flutter. אחת השיטות הכי קלות להסבר היא ChangeNotifier
, הגישה שהאפליקציה הזו נוקטת.
- הפרמטר
MyAppState
מגדיר את הנתונים שהאפליקציה צריכה כדי לפעול. בשלב הזה, יש בו רק משתנה אחד עם צמד המילים האקראי הנוכחי. יתווסף אליהם בהמשך. - סיווג המדינה תוארך ב-
ChangeNotifier
, כלומר הוא יכול ליידע אחרים על השינויים שלו. לדוגמה, אם צמד המילים הנוכחי משתנה, חלק מהווידג'טים באפליקציה צריכים לדעת על כך. - המדינה (State) נוצרת ומסופקת לכל האפליקציה באמצעות
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
ב-Codelab הזה, אבל הוא ווידג'ט מועיל שנמצא ברוב האפליקציות של Flutter בעולם האמיתי. Column
הוא אחד מהווידג'טים הבסיסיים ביותר של הפריסה ב-Flutter. היא לוקחת כל מספר של צאצאים ומציבה אותם בעמודה מלמעלה למטה. כברירת מחדל, העמודה מציבה את הצאצאים שלה בחלק העליון של הדף. בקרוב משנים את המיקום כך שהעמודה תהיה במרכז.- שינית את הווידג'ט הזה של
Text
בשלב הראשון. - הווידג'ט השני של
Text
מקבלappState
, והוא ניגש לחבר היחיד בכיתה,current
(כלומר,WordPair
).WordPair
יש כמה רעיונות שימושיים, כמוasPascalCase
אוasSnakeCase
. כאן אנחנו משתמשים בasLowerCase
, אבל אפשר לשנות את ההגדרה הזו עכשיו אם ברצונך להשתמש באחת מהחלופות. - שימו לב שבקוד של Flutter נעשה שימוש רב בפסיקים. לא צריך להוסיף את הפסיק הספציפי הזה, כי
children
הוא החבר האחרון (וגם הרק) ביותר ברשימת הפרמטרים הספציפית שלColumn
. עם זאת, באופן כללי כדאי להשתמש בפסיקים בסוף: ההוספה של עוד חברים היא פעולה מזערית, והם גם רמזו על כך שהמעצב האוטומטי של דרט יוסיף שם שורה חדשה. מידע נוסף מופיע במאמר בנושא עיצוב קוד.
בשלב הבא, מחברים את הלחצן למצב.
ההתנהגות הראשונה שלך
גוללים אל 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 מספקת כלי לארגון הקוד מחדש (Refactoring) לחילוץ ווידג'טים, אבל לפני שמשתמשים בו, חשוב לוודא שהשורה שחולפת ניגשת רק למה שצריך. כרגע הקו ניגש אל 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
במקרה הזה) ובוחרים באפשרות Refactor... מהתפריט הנפתח.
או
- מעבירים את הסמן לקטע הקוד שרוצים להגדיר מחדש (
Text
, במקרה זה) ולוחצים עלCtrl+.
(Win/Linux) או עלCmd+.
(Mac).
בתפריט Refactor, בוחרים באפשרות חילוץ Widget. מקצים שם, כמו 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()
שבה. כמו קודם, אפשר לחזור לתפריט Refactoring בווידג'ט Text
. עם זאת, הפעם לא מתכוננים לחלץ את הווידג'ט.
במקום זאת, בוחרים באפשרות גלישה עם מרווח פנימי. הפעולה הזו יוצרת ווידג'ט הורה חדש מסביב לווידג'ט Text
בשם Padding
. לאחר השמירה, תראו שלמילה האקראית כבר יש יותר מרחב נשימה.
הגדלת המרווח הפנימי מערך ברירת המחדל 8.0
. לדוגמה, אפשר להוסיף משהו כמו 20
למרווח פנימי גדול יותר.
עכשיו מתקדמים לרמה אחת יותר. מציבים את הסמן על הווידג'ט Padding
, פותחים את התפריט Refactoring ובוחרים באפשרות גלישת טקסט באמצעות ווידג'ט....
כך ניתן לציין את ווידג'ט ההורה. מקלידים 'כרטיס'. ומקישים על 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
. Dat, שפת התכנות שבה כותבים את האפליקציה הזו היא אפסית, ולכן היא לא מאפשרת לקרוא לשיטות של אובייקטים שעשויים להיותnull
. עם זאת, במקרה הזה אפשר להשתמש באופרטור!
("אופרטור bang") כדי לוודא ש-Dart יודע מה אתה עושה. (במקרה הזה,displayMedium
בהחלט לא ריק. עם זאת, אנחנו יודעים שהדבר חורג מההיקף של ה-Codelab הזה.) - קריאה אל
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
. כברירת מחדל, עמודות מובילות את הצאצאים שלהן לראש הדף, אבל אנחנו יכולים לעקוף זאת בקלות. צריך לעבור ל-method 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
. נוכל לבדוק את זה באמצעות הכלי לבדיקת ווידג'טים.
הכלי לבדיקת ווידג'טים חורג מההיקף של ה-Codelab הזה, אבל אפשר לראות שכאשר ה-Column
מודגש, הוא לא תופס את כל רוחב האפליקציה. היא תופסת שטח אופקי רק ככל שהילדים שלה צריכים.
אפשר פשוט למרכז את העמודה עצמה. מציבים את הסמן על Column
, פותחים את התפריט Refactoring (עם Ctrl+.
או Cmd+.
) ובוחרים באפשרות גלישת טקסט באמצעות המרכז.
עכשיו האפליקציה אמורה להיראות כך:
אם רוצים, אפשר לשנות קצת יותר.
- אפשר להסיר את הווידג'ט
Text
שמעלBigCard
. אפשר לטעון שהטקסט התיאורי ("רעיון אקראי AWESOME:") כבר לא נחוץ, כי ממשק המשתמש הגיוני גם בלעדיו. כך הוא נקייה יותר. - אפשר גם להוסיף ווידג'ט
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'),
),
],
),
),
);
}
}
// ...
והאפליקציה נראית כך:
בקטע הבא נוסיף את האפשרות לסמן מילים כ'מועדפים' (או לסמן 'לייק') על מילים שנוצרו על ידי AI.
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
. עוברים לשיטת build()
של MyHomePage
, ממקמים את הסמן על ElevatedButton
, מגדירים את התפריט Refactoring באמצעות 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
. הפעם משתמשים ב-constructor של 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
ל-2 ווידג'טים נפרדים.
יש לבחור את כל שירותי 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
של רכבת הניווט. מומלץ גם לשנות את הערך הזה מתוך הקריאה החוזרת (callback) של onDestinationSelected
.
ניתן להוסיף את selectedIndex
בתור עוד נכס של MyAppState
. וזה יעבוד. אבל אפשר לדמיין שמצב האפליקציה יתארך במהירות מעבר לסיבה, אם כל ווידג'ט ישמור בו את הערכים שלו.
חלק מהמצבים האלה רלוונטיים רק לווידג'ט אחד, לכן הוא צריך להישאר עם אותו ווידג'ט.
צריך להזין את StatefulWidget
, סוג של ווידג'ט שכולל את State
. קודם כול צריך להמיר את MyHomePage
לווידג'ט עם שמירת מצב.
מציבים את הסמן על השורה הראשונה של השדה MyHomePage
(שמתחילה ב-class MyHomePage...
) וקוראים לתפריט Refactoring באמצעות Ctrl+.
או Cmd+.
. לאחר מכן, בוחרים באפשרות Convert to StatefulWidget (המרה ל-StatefulWidget).
סביבת הפיתוח המשולבת (IDE) יוצרת כיתה חדשה בשבילך, _MyHomePageState
. הקורס הזה נמשך State
ולכן הוא יכול לנהל את הערכים שלו. (הוא יכול לשנות את עצמו). כדאי לשים לב גם ששיטת build
מהווידג'ט הישן וללא שמירת מצב הועברה אל _MyHomePageState
(במקום להישאר בווידג'ט). היא הועברה מילה במילה, ושום דבר לא השתנה בשיטה build
. עכשיו הוא פשוט גר במקום אחר.
setState (הגדרת מצב)
לווידג'ט החדש עם שמירת המצב צריך לעקוב רק אחרי משתנה אחד: selectedIndex
. מבצעים את 3 השינויים הבאים ב-_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
לבין ה-placeholder שיהפוך בקרוב לדף מועדפים.
תגובה למשתמשים
לאחר מכן, להפוך את רכבת הניווט לרספונסיבית. כלומר, להגדיר שהתוויות יוצגו באופן אוטומטי (באמצעות extended: true
) כשיש מספיק מקום לתמונות.
Flutter מספקת כמה ווידג'טים שעוזרים להפוך את האפליקציות לרספונסיביות באופן אוטומטי. לדוגמה, Wrap
הוא ווידג'ט שדומה ל-Row
או ל-Column
שעוטף ילדים באופן אוטומטי ל'שורה' הבאה (שנקרא 'הרצה') כשאין מספיק שטח אנכי או אופקי. יש את FittedBox
, ווידג'ט שמתאים אוטומטית לילד או לילדה בשטח הזמין לפי המפרט שלכם.
עם זאת, NavigationRail
לא מציג תוויות באופן אוטומטי כשיש מספיק מקום כי אין לו אפשרות לדעת מה יש מספיק מקום בכל הקשר. באחריותך, המפתח, לבצע את השיחה.
נניח שמחליטים להציג תוויות רק אם הרוחב של MyHomePage
הוא לפחות 600 פיקסלים.
הווידג'ט שבו צריך להשתמש, במקרה הזה, הוא LayoutBuilder
. ניתן לשנות את עץ הווידג'ט בהתאם לנפח האחסון הזמין.
שוב, משתמשים בתפריט Refactor של Flutter ב-VS Code כדי לבצע את השינויים הנדרשים. הפעם זה קצת יותר מורכב:
- בתוך השיטה
build
של_MyHomePageState
, מציבים את הסמן עלScaffold
. - קוראים לתפריט Refactor דרך התפריט
Ctrl+.
(Windows/Linux) אוCmd+.
(Mac). - בוחרים באפשרות גלישה עם Builder ומקישים על Enter.
- שינוי השם של האפליקציה
Builder
החדשה שהוספת אלLayoutBuilder
. - שינוי רשימת הפרמטרים של קריאה חוזרת מ-
(context)
ל-(context, constraints)
.
הקריאה החוזרת (callback) של LayoutBuilder
מופעלת בכל פעם שהאילוצים משתנים.builder
זה קורה כאשר, למשל:
- המשתמש משנה את גודל החלון של האפליקציה
- המשתמש מסובב את הטלפון מפריסה לאורך לפריסה לרוחב או שחוזר אחורה
- חלק מהווידג'טים ליד
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
.
היתרון של הוספת הדף מועדפים הוא קבלת מידע נוסף באמצעות קבלת החלטות משלכם. החיסרון הוא מצב שבו אתם עלולים להיתקל בבעיות שאתם עדיין לא מצליחים לפתור בעצמכם. זכרו: להיכשל הוא בסדר, והוא אחד מרכיבי הלמידה החשובים ביותר בלמידה. אף אחד לא מצפה ממך לפתח ציפורניים בשעה הראשונה, וגם לא לך.
השלבים הבאים הם רק דרך אחת להטמיע את דף המועדפים. אופן ההטמעה של הקוד (אני מקווה) יעודד אתכם לשחק עם הקוד – יעזור לכם לשפר את ממשק המשתמש ולהפוך אותו לאישי.
הנה הכיתה החדשה ב-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
- זה הזמן להגדיר את האפליקציה רספונסיבית
- השגת מראה עקבי של האפליקציה
מה הדבר הבא?
- כדאי להתנסות באפליקציה שכתבת במהלך שיעור ה-Lab הזה.
- בודקים את הקוד של הגרסה המתקדמת הזו של אותה האפליקציה כדי לראות איך להוסיף רשימות עם אנימציה, הדרגתיות, עמעום הדרגתי ועוד.
- כדי לעקוב אחר תהליך הלמידה, אפשר להיכנס לכתובת flutter.dev/learn.