1. מבוא
Flutter היא ערכת הכלים של Google לפיתוח ממשקי משתמש. היא מאפשרת לפתח אפליקציות למכשירים ניידים, לאינטרנט ולמחשבים – מ-codebase יחיד. בשיעור הזה תלמדו איך ליצור את אפליקציית Flutter הבאה:
האפליקציה יוצרת שמות שנשמעים מגניבים, כמו newstay, lightstream, mainbrake או graypine. המשתמש יכול לבקש את השם הבא, להוסיף את השם הנוכחי למועדפים ולעיין ברשימת השמות המועדפים בדף נפרד. האפליקציה רספונסיבית לגדלים שונים של מסכים.
מה תלמדו?
- הסבר בסיסי על אופן הפעולה של Flutter
- יצירת פריסות ב-Flutter
- קישור בין אינטראקציות של משתמשים (כמו לחיצות על לחצנים) לבין התנהגות האפליקציה
- שמירה על סדר בקוד Flutter
- התאמת האפליקציה למסכים שונים
- איך יוצרים מראה ותחושה עקביים באפליקציה
תקבלו מסגרת בסיסית כדי שתוכלו לדלג ישר לחלקים המעניינים.
בסרטון הבא, Filip מסביר על כל ה-codelab.
לוחצים על 'הבא' כדי להתחיל את המעבדה.
2. הגדרת סביבת Flutter
הרשאת עריכה
כדי שה-codelab הזה יהיה פשוט ככל האפשר, אנחנו מניחים שתשתמשו ב-Visual Studio Code (VS Code) כסביבת הפיתוח שלכם. הוא בחינם ופועל בכל הפלטפורמות העיקריות.
אפשר להשתמש בכל עורך שרוצים: Android Studio, IntelliJ IDEs, Emacs, Vim או Notepad++. כולם עובדים עם Flutter.
מומלץ להשתמש ב-VS Code בשביל ה-codelab הזה, כי ההוראות מוגדרות כברירת מחדל לקיצורי דרך ספציפיים ל-VS Code. יותר קל לומר דברים כמו "לחץ כאן" או "לחץ על המקש הזה" במקום לומר משהו כמו "בצע את הפעולה המתאימה בעורך כדי לבצע את הפעולה X".
בחירת יעד פיתוח
Flutter הוא ערכת כלים חוצת פלטפורמות. האפליקציה יכולה לפעול בכל אחת ממערכות ההפעלה הבאות:
- iOS
- Android
- Windows
- macOS
- Linux
- אינטרנט
עם זאת, נהוג לבחור מערכת הפעלה אחת שבה בעיקר תפתחו את האפליקציה. זוהי 'מערכת ההפעלה לפיתוח' – מערכת ההפעלה שהאפליקציה פועלת בה במהלך הפיתוח.
לדוגמה, נניח שאתם משתמשים במחשב נייד עם Windows כדי לפתח אפליקציית Flutter. אם תבחרו ב-Android כיעד הפיתוח, בדרך כלל תחברו מכשיר Android למחשב הנייד עם Windows באמצעות כבל USB, והאפליקציה שבפיתוח תפעל במכשיר Android המחובר. אבל אפשר גם לבחור ב-Windows כיעד הפיתוח, כלומר האפליקציה שנמצאת בפיתוח תפעל כאפליקציית Windows לצד העורך.
יכול להיות שתתפתו לבחור באינטרנט כיעד הפיתוח. החיסרון בבחירה הזו הוא שמאבדים את אחד מפיצ'רי הפיתוח הכי שימושיים של Flutter: טעינה מחדש של מצב (Stateful Hot Reload). אי אפשר לבצע טעינה מחדש של אפליקציות אינטרנט ב-Flutter.
בוחרים עכשיו. חשוב לזכור: תמיד אפשר להריץ את האפליקציה במערכות הפעלה אחרות בהמשך. פשוט, אם יש לכם יעד פיתוח ברור בראש, השלב הבא יהיה חלק יותר.
התקנת Flutter
ההוראות העדכניות ביותר להתקנת Flutter SDK זמינות תמיד בכתובת docs.flutter.dev.
ההוראות באתר Flutter כוללות לא רק את ההתקנה של ה-SDK עצמו, אלא גם את הכלים שקשורים ליעד הפיתוח ואת התוספים לעורך. חשוב לזכור שבקוד לאב הזה צריך להתקין רק את הרכיבים הבאים:
- Flutter SDK
- קוד Visual Studio עם הפלאגין של Flutter
- התוכנה שנדרשת ליעד הפיתוח שבחרתם (לדוגמה: Visual Studio לטירגוט Windows או Xcode לטירגוט macOS)
בקטע הבא, תיצרו את פרויקט Flutter הראשון שלכם.
אם נתקלת בבעיות עד עכשיו, יכול להיות שחלק מהשאלות והתשובות האלה (מ-StackOverflow) יעזרו לך לפתור אותן.
שאלות נפוצות
- איך מוצאים את הנתיב של Flutter SDK?
- מה עושים אם הפקודה Flutter לא נמצאה?
- איך פותרים את הבעיה 'Waiting for another flutter command to release the startup lock' (המתנה לפקודת Flutter אחרת כדי לבטל את נעילת ההפעלה)?
- איך מציינים ב-Flutter את המיקום של Android SDK?
- איך מתמודדים עם שגיאת Java כשמפעילים את
flutter doctor --android-licenses
? - איך מתמודדים עם השגיאה 'לא נמצא כלי Android
sdkmanager
'? - איך מתמודדים עם השגיאה 'הרכיב
cmdline-tools
חסר'? - איך מריצים CocoaPods ב-Apple Silicon (M1)?
- איך משביתים את העיצוב האוטומטי בשמירה ב-VS Code?
3. יצירת פרויקט
יצירת פרויקט Flutter ראשון
מפעילים את Visual Studio Code ופותחים את לוח הפקודות (באמצעות F1
או Ctrl+Shift+P
או Shift+Cmd+P
). מתחילים להקליד flutter new. בוחרים בפקודה Flutter: New Project (Flutter: פרויקט חדש).
לאחר מכן בוחרים באפשרות Application (אפליקציה) ואז בתיקייה שבה רוצים ליצור את הפרויקט. זה יכול להיות ספריית הבית או משהו כמו C:\src\
.
ולבסוף, נותנים שם לפרויקט. משהו כמו namer_app
או my_awesome_namer
.
מערכת Flutter יוצרת עכשיו את תיקיית הפרויקט ופותחת אותה ב-VS Code.
עכשיו תדרוס את התוכן של 3 קבצים עם פיגום בסיסי של האפליקציה.
העתקה והדבקה של האפליקציה הראשונית
בחלונית הימנית של VS Code, מוודאים שהאפשרות Explorer (סייר) מסומנת, ופותחים את הקובץ pubspec.yaml
.
מחליפים את התוכן של הקובץ בתוכן הבא:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
קובץ ה-pubspec.yaml
מציין מידע בסיסי על האפליקציה, כמו הגרסה הנוכחית שלה, התלות שלה בתוספים והנכסים שייכללו בה.
לאחר מכן, פותחים קובץ הגדרה נוסף בפרויקט, analysis_options.yaml
.
מחליפים את התוכן שלו בתוכן הבא:
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
הקובץ הזה קובע עד כמה Flutter צריך להיות קפדני בניתוח הקוד. זו הפעם הראשונה שאתם משתמשים ב-Flutter, ולכן אתם אומרים לכלי הניתוח לא להגזים. תמיד אפשר לשנות את ההגדרה הזו בהמשך. למעשה, ככל שתתקרבו לפרסום אפליקציה אמיתית בסביבת ייצור, כמעט בטוח שתצטרכו להגדיר את הכלי לניתוח כקפדני יותר.
לבסוף, פותחים את הקובץ main.dart
בספרייה lib/
.
מחליפים את התוכן של הקובץ בתוכן הבא:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
50 שורות הקוד האלה הן כל מה שיש באפליקציה עד עכשיו.
בקטע הבא, מריצים את האפליקציה במצב ניפוי באגים ומתחילים לפתח.
4. הוספת כפתור
בשלב הזה מוסיפים לחצן הבא כדי ליצור צמד מילים חדש.
הפעלת האפליקציה
קודם כול, פותחים את lib/main.dart
ומוודאים שמכשיר היעד נבחר. בפינה השמאלית התחתונה של VS Code, יש לחצן שבו מוצג מכשיר היעד הנוכחי. כדי לשנות את ההגדרה, לוחצים עליה.
בזמן ש-lib/main.dart
פתוח, מחפשים את לחצן ההפעלה בפינה השמאלית העליונה של חלון VS Code ולוחצים עליו.
אחרי דקה בערך, האפליקציה מופעלת במצב ניפוי באגים. התוצאה לא נראית מרשימה במיוחד:
הפעלת Hot Reload בפעם הראשונה
בחלק התחתון של 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),
],
),
);
// ...
שימו לב שהאפליקציה משתנה באופן מיידי, אבל המילה האקראית נשארת ללא שינוי. זהו טעינה מחדש של מצב (stateful Hot Reload) המפורסם של Flutter בפעולה. טעינה מחדש מהירה מופעלת כששומרים שינויים בקובץ מקור.
שאלות נפוצות
- מה קורה אם התכונה 'טעינה מחדש פעילה' לא פועלת ב-VSCode?
- האם צריך ללחוץ על r כדי לבצע טעינה חמה ב-VSCode?
- האם Hot Reload פועל באינטרנט?
- איך מסירים את באנר הניפוי באגים?
הוספת כפתור
לאחר מכן, מוסיפים לחצן בתחתית 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 מופיעה ההודעה button pressed! (הלחצן נלחץ).
קורס מזורז ב-Flutter ב-5 דקות
אמנם כיף לצפות במסוף ניפוי הבאגים, אבל אתם רוצים שהלחצן יעשה משהו משמעותי יותר. אבל לפני כן, כדאי לבחון את הקוד ב-lib/main.dart
כדי להבין איך הוא פועל.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
בחלק העליון של הקובץ, תופיע הפונקציה main()
. במצב הנוכחי, הפקודה הזו רק אומרת ל-Flutter להריץ את האפליקציה שמוגדרת ב-MyApp
.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
הכיתה MyApp
extends StatelessWidget
. ווידג'טים הם הרכיבים שמהם בונים כל אפליקציית Flutter. כמו שאתם רואים, אפילו האפליקציה עצמה היא ווידג'ט.
הקוד ב-MyApp
מגדיר את כל האפליקציה. הוא יוצר את המצב של האפליקציה (מידע נוסף על כך בהמשך), נותן שם לאפליקציה, מגדיר את העיצוב החזותי ומגדיר את הווידג'ט 'דף הבית' – נקודת ההתחלה של האפליקציה.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
בשלב הבא, המחלקה MyAppState
מגדירה את המצב של האפליקציה. זו הפעם הראשונה שאתם מתנסים ב-Flutter, ולכן ה-codelab הזה יהיה פשוט וממוקד. יש הרבה דרכים יעילות לנהל את מצב האפליקציה ב-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
ב-codelab הזה, אבל זה ווידג'ט שימושי שנמצא ברוב האפליקציות של Flutter בעולם האמיתי. -
Column
הוא אחד מווידג'טים הפריסה הבסיסיים ביותר ב-Flutter. הוא מקבל מספר כלשהו של ילדים ומציב אותם בעמודה מלמעלה למטה. כברירת מחדל, הצאצאים של העמודה מוצגים בחלק העליון. בקרוב תשנו את זה כך שהעמודה תהיה מיושרת למרכז. - שינית את הווידג'ט
Text
בשלב הראשון. - ווידג'ט
Text
השני מקבל אתappState
וניגש לחבר היחיד בכיתה הזו,current
(שהואWordPair
).WordPair
מספק כמה שיטות getter שימושיות, כמו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
במקרה הזה) ובוחרים באפשרות Refactor... (רפקטורינג) מהתפריט הנפתח.
או
- מעבירים את הסמן לקטע הקוד שרוצים לבצע בו רפקטורינג (
Text
במקרה הזה) ומקישים עלCtrl+.
(ב-Windows/Linux) או עלCmd+.
(ב-Mac).
בתפריט Refactor, בוחרים באפשרות Extract 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()
בתוכה. כמו קודם, מציגים את התפריט Refactor בווידג'ט Text
. אבל הפעם לא תחלצו את הווידג'ט.
במקום זאת, בוחרים באפשרות הוספת מרווחים מסביב. הפעולה הזו יוצרת ווידג'ט אב חדש מסביב לווידג'ט Text
בשם Padding
. אחרי השמירה, תראו שהמילה האקראית כבר לא צמודה למילה הקודמת.
הגדלת הריווח הפנימי מערך ברירת המחדל 8.0
. לדוגמה, אפשר להשתמש בערך כמו 20
כדי להגדיל את המרווח הפנימי.
לאחר מכן, עולים רמה אחת למעלה. מציבים את הסמן על הווידג'ט Padding
, פותחים את התפריט Refactor (שינוי מבנה) ובוחרים באפשרות Wrap with widget... (הוספת ווידג'ט מסביב...).
כך תוכלו לציין את הווידג'ט הראשי. מקלידים Card ומקישים על Enter.
הווידג'ט Card
עוטף את הווידג'ט Padding
, ולכן גם את הווידג'ט Text
.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
האפליקציה תיראה עכשיו בערך כך:
עיצוב וסגנון
כדי שהכרטיס יבלוט יותר, צובעים אותו בצבע עשיר יותר. כדאי לשמור על ערכת צבעים עקבית, ולכן מומלץ להשתמש בTheme
של האפליקציה כדי לבחור את הצבע.
מבצעים את השינויים הבאים בשיטה של BigCard
build()
.
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-safe, ולכן לא תוכלו להפעיל מתודות של אובייקטים שעשויים להיותnull
. עם זאת, במקרה הזה אפשר להשתמש באופרטור!
(אופרטור קריאה) כדי להבהיר ל-Dart שאתם יודעים מה אתם עושים. (במקרה הזה,displayMedium
הוא בוודאות לא null. הסיבה לכך לא מוסברת ב-Codelab הזה). - הפונקציה
copyWith()
שמופעלת עלdisplayMedium
מחזירה עותק של סגנון הטקסט עם השינויים שהגדרתם. במקרה הזה, משנים רק את צבע הטקסט. - כדי לקבל את הצבע החדש, צריך שוב לגשת לערכת הנושא של האפליקציה. המאפיין
onPrimary
של ערכת הצבעים מגדיר צבע שמתאים לשימוש על הצבע הראשי של האפליקציה.
האפליקציה אמורה להיראות עכשיו בערך כך:
אם רוצים, אפשר לשנות עוד את הכרטיס. רעיונות לשינויים:
copyWith()
מאפשר לשנות הרבה יותר מאשר רק את הצבע של סגנון הטקסט. כדי לראות את הרשימה המלאה של המאפיינים שאפשר לשנות, מציבים את הסמן בכל מקום בתוך הסוגריים שלcopyWith()
, ומקישים עלCtrl+Shift+Space
(Windows/Linux) או עלCmd+Shift+Space
(Mac).- באופן דומה, אפשר לשנות עוד דברים בווידג'ט של
Card
. לדוגמה, אפשר להגדיל את הצל של הכרטיס על ידי הגדלת הערך של הפרמטרelevation
. - כדאי להתנסות עם צבעים שונים. בנוסף ל-
theme.colorScheme.primary
, יש גם את.secondary
, את.surface
ועוד רבים אחרים. לכל הצבעים האלה יש מקבילות ב-onPrimary
.
שיפור הנגישות
אפליקציות ב-Flutter נגישות כברירת מחדל. לדוגמה, כל אפליקציית Flutter מציגה בצורה נכונה את כל הטקסט והרכיבים האינטראקטיביים באפליקציה לקוראי מסך כמו TalkBack ו-VoiceOver.
אבל לפעמים צריך לעשות קצת עבודה. במקרה של האפליקציה הזו, יכול להיות שלקורא המסך יהיו בעיות בהגיית חלק מזוגות המילים שנוצרו. בני אדם לא מתקשים לזהות את שתי המילים ב-cheaphead, אבל קורא מסך עלול להגות את הצירוף ph באמצע המילה כ-f.
פתרון אפשרי הוא להחליף את pair.asLowerCase
ב-"${pair.first} ${pair.second}"
. האחרון משתמש באינטרפולציה של מחרוזות כדי ליצור מחרוזת (כמו "cheap head"
) משתי המילים שמופיעות ב-pair
. שימוש בשתי מילים נפרדות במקום במילה מורכבת מבטיח שקוראי המסך יזהו אותן בצורה נכונה, ומספק חוויה טובה יותר למשתמשים עם ליקויי ראייה.
עם זאת, יכול להיות שתרצו לשמור על הפשטות החזותית של pair.asLowerCase
. משתמשים במאפיין Text
של semanticsLabel
כדי להחליף את התוכן החזותי של ווידג'ט הטקסט בתוכן סמנטי שמתאים יותר לקוראי מסך:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
עכשיו, קוראי המסך מבטאים נכון כל צמד מילים שנוצר, אבל ממשק המשתמש נשאר זהה. כדאי לנסות את הפעולה הזו בעזרת קורא מסך במכשיר.
מרכוז ממשק המשתמש
עכשיו, אחרי שהצגנו את צמד המילים האקראי עם מספיק אלמנטים ויזואליים, הגיע הזמן למקם אותו במרכז החלון או המסך של האפליקציה.
קודם כל, חשוב לזכור שBigCard
הוא חלק מColumn
. כברירת מחדל, העמודות מציגות את הילדים שלהן בחלק העליון, אבל אפשר לשנות את זה. עוברים לשיטה MyHomePage
של build()
ומבצעים את השינוי הבא:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
המאפיין הזה ממקם את רכיבי הצאצא במרכז של Column
לאורך הציר הראשי (האנכי).
הילדים כבר ממוקמים במרכז לאורך ציר הצלב של העמודה (כלומר, הם כבר ממוקמים במרכז באופן אופקי). אבל Column
הסרטון עצמו לא ממוקם במרכז בתוך Scaffold
. אפשר לבדוק את זה באמצעות הכלי לבדיקת ווידג'טים.
השימוש ב-Widget Inspector חורג מהיקף ה-codelab הזה, אבל אפשר לראות שכשהרכיב Column
מודגש, הוא לא תופס את כל הרוחב של האפליקציה. הוא תופס רק את המרחב האופקי שנדרש לרכיבי הצאצא שלו.
אפשר פשוט למרכז את העמודה עצמה. מציבים את הסמן על Column
, פותחים את התפריט Refactor (עם Ctrl+.
או Cmd+.
) ובוחרים באפשרות Wrap with Center.
האפליקציה אמורה להיראות עכשיו בערך כך:
אם רוצים, אפשר לבצע עוד שינויים.
- אפשר להסיר את הווידג'ט
Text
שמופיע מעלBigCard
. אפשר לטעון שהטקסט התיאורי ("רעיון אקראי ומדהים:") כבר לא נחוץ, כי ממשק המשתמש ברור גם בלעדיו. וכך זה יותר נקי. - אפשר גם להוסיף ווידג'ט
SizedBox(height: 10)
ביןBigCard
לביןElevatedButton
. כך יש יותר הפרדה בין שני הווידג'טים. הווידג'טSizedBox
רק תופס מקום ולא מציג שום דבר בעצמו. בדרך כלל משתמשים בו כדי ליצור 'פערים' חזותיים.
אחרי השינויים האופציונליים, הקוד הבא מופיע ב-MyHomePage
:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
האפליקציה תיראה כך:
בקטע הבא, תוסיפו אפשרות לסמן מילים שנוצרו כמועדפות (או'אהובות').
6. הוספת פונקציונליות
האפליקציה עובדת, ולפעמים היא אפילו מספקת צמדי מילים מעניינים. אבל בכל פעם שהמשתמש לוחץ על הבא, כל צמד מילים נעלם לתמיד. יהיה טוב יותר אם תהיה דרך 'לזכור' את ההצעות הכי טובות, למשל לחצן 'אהבתי'.
הוספת הלוגיקה העסקית
גוללים אל MyAppState
ומוסיפים את הקוד הבא:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
בודקים את השינויים:
- הוספת נכס חדש לחשבון
MyAppState
בשםfavorites
. הנכס הזה מאותחל עם רשימה ריקה:[]
. - ציינתם גם שהרשימה יכולה להכיל רק צמדי מילים:
<WordPair>[]
, באמצעות גנריקה. כך האפליקציה תהיה חזקה יותר – Dart לא יאפשר אפילו להריץ את האפליקציה אם תנסו להוסיף לה משהו שהוא לאWordPair
. כך תוכלו להשתמש ברשימתfavorites
ולדעת שלא יכולים להיות בה אובייקטים לא רצויים (כמוnull
).
- הוספתם גם שיטה חדשה,
toggleFavorite()
, שמסירה את צמד המילים הנוכחי מרשימת המועדפים (אם הוא כבר נמצא שם) או מוסיפה אותו (אם הוא עדיין לא נמצא שם). בכל מקרה, הקוד קורא ל-notifyListeners();
לאחר מכן.
הוספת הכפתור
אחרי שסיימנו עם הלוגיקה העסקית, הגיע הזמן לעבוד שוב על ממשק המשתמש. כדי להציב את הלחצן 'אהבתי' משמאל ללחצן 'הבא' צריך Row
. הווידג'ט Row
הוא המקבילה האופקית של Column
, שראיתם קודם.
קודם כול, עוטפים את הלחצן הקיים בתג Row
. עוברים לשיטה MyHomePage
של build()
, מציבים את הסמן על ElevatedButton
, מציגים את התפריט Refactor באמצעות Ctrl+.
או Cmd+.
, ובוחרים באפשרות Wrap with Row.
כששומרים, אפשר לראות שההתנהגות של Row
דומה לזו של Column
– כברירת מחדל, הוא מקבץ את הצאצאים שלו בצד ימין. (Column
lumped its children to the top.) כדי לפתור את הבעיה, אפשר להשתמש באותה גישה כמו קודם, אבל עם 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
הנוכחי. אם בוחרים באינדקס 0, היעד הראשון נבחר. אם בוחרים באינדקס 1, היעד השני נבחר וכן הלאה. בשלב הזה, הערך מוגדר כ-0. - בנוסף, סרגל הניווט מגדיר מה קורה כשהמשתמש בוחר באחד היעדים באמצעות
onDestinationSelected
. בשלב הזה, האפליקציה רק מוציאה את ערך האינדקס המבוקש באמצעותprint()
. - הצאצא השני של
Row
הוא הווידג'טExpanded
. ווידג'טים מורחבים שימושיים מאוד בשורות ובעמודות – הם מאפשרים לכם ליצור פריסות שבהן חלק מהרכיבים התחתונים תופסים רק את המקום שהם צריכים (SafeArea
במקרה הזה), ווידג'טים אחרים תופסים כמה שיותר מהמקום שנותר (Expanded
במקרה הזה). אחת הדרכים להבין את הווידג'טים שלExpanded
היא לחשוב עליהם כעל "חמדנים". כדי להבין טוב יותר את התפקיד של הווידג'ט הזה, אפשר לנסות להוסיף עודExpanded
לווידג'טSafeArea
. הפריסה שמתקבלת נראית בערך כך:
- שני ווידג'טים
Expanded
מחלקים ביניהם את כל השטח האופקי הזמין, למרות שסרגל הניווט צריך רק חלק קטן בצד שמאל. - בווידג'ט
Expanded
יש צורה צבעוניתContainer
, ובתוך הקונטיינר ישGeneratorPage
.
ווידג'טים בלי שמירת מצב לעומת ווידג'טים עם שמירת מצב
עד עכשיו, MyAppState
סיפק את כל הצרכים שלכם במדינה. לכן כל הווידג'טים שכתבתם עד עכשיו הם חסרי מצב. הם לא מכילים מצב משתנה משלהם. אף אחד מהווידג'טים לא יכול לשנות את עצמו – הם חייבים לעבור דרך MyAppState
.
אבל זה עומד להשתנות.
צריך למצוא דרך לשמור את הערך של selectedIndex
של סרגל הניווט. אתם רוצים גם לשנות את הערך הזה מתוך הקריאה החוזרת (callback) של onDestinationSelected
.
אפשר להוסיף את selectedIndex
כנכס נוסף של MyAppState
. וזה יעבוד. אבל אפשר לדמיין שמצב האפליקציה יגדל במהירות מעבר לכל היגיון אם כל ווידג'ט יאחסן בו את הערכים שלו.
חלק מהמצבים רלוונטיים רק לווידג'ט אחד, ולכן הם צריכים להישאר בווידג'ט הזה.
מזינים את StatefulWidget
, סוג של ווידג'ט שכולל State
. קודם ממירים את MyHomePage
לווידג'ט עם מצב.
מציבים את הסמן בשורה הראשונה של MyHomePage
(השורה שמתחילה ב-class MyHomePage...
), ומציגים את התפריט Refactor באמצעות Ctrl+.
או Cmd+.
. לאחר מכן בוחרים באפשרות Convert to StatefulWidget (המרה ל-StatefulWidget).
סביבת הפיתוח המשולבת יוצרת בשבילכם מחלקה חדשה, _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
שהיה שם עד עכשיו. - כשקוראים לפונקציית ה-callback
onDestinationSelected
, במקום להדפיס את הערך החדש במסוף, מקצים אותו ל-selectedIndex
בתוך קריאה ל-setState()
. הקריאה הזו דומה לשיטתnotifyListeners()
ששימשה בעבר – היא מוודאת שממשק המשתמש מתעדכן.
סרגל הניווט מגיב עכשיו לאינטראקציה של המשתמש. אבל האזור המורחב בצד שמאל יישאר ללא שינוי. הסיבה לכך היא שהקוד לא משתמש ב-selectedIndex
כדי לקבוע איזה מסך יוצג.
שימוש ב-selectedIndex
ממקמים את הקוד הבא בחלק העליון של השיטה _MyHomePageState
build
, ממש לפני return Scaffold
:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
עיין בקטע הקוד הזה:
- הקוד מכריז על משתנה חדש,
page
, מהסוגWidget
. - לאחר מכן, משפט switch מקצה מסך ל-
page
, בהתאם לערך הנוכחי ב-selectedIndex
. - מכיוון שאין עדיין
FavoritesPage
, אפשר להשתמש ב-Placeholder
– ווידג'ט שימושי שמצייר מלבן עם קו אלכסוני בכל מקום שבו מציבים אותו, כדי לסמן את החלק הזה בממשק המשתמש כלא גמור.
- בהתאם לעיקרון fail-fast, משפט ה-switch מוודא גם שתוצג שגיאה אם
selectedIndex
הוא לא 0 ולא 1. כך אפשר למנוע באגים בהמשך. אם מוסיפים יעד חדש לסרגל הניווט ושוכחים לעדכן את הקוד הזה, התוכנית קורסת במהלך הפיתוח (במקום לנחש למה הדברים לא עובדים, או לפרסם קוד עם באגים בייצור).
עכשיו, כשpage
מכיל את הווידג'ט שרוצים להציג בצד ימין, אפשר לנחש מה השינוי הנוסף שצריך לבצע.
כך נראה _MyHomePageState
אחרי השינוי היחיד שנותר:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
האפליקציה עוברת עכשיו בין GeneratorPage
לבין ה-placeholder שיהפוך בקרוב לדף מועדפים.
תגובה למשתמשים
לאחר מכן, הופכים את סרגל הניווט לרספונסיבי. כלומר, להגדיר שהתוויות יוצגו אוטומטית (באמצעות extended: true
) כשיש מספיק מקום להצגה שלהן.
Flutter מספקת כמה ווידג'טים שעוזרים לכם ליצור אפליקציות רספונסיביות באופן אוטומטי. לדוגמה, Wrap
הוא ווידג'ט שדומה ל-Row
או ל-Column
, שמציג את הצאצאים בשורה הבאה (שנקראת 'run') באופן אוטומטי כשאין מספיק מקום אנכי או אופקי. יש גם את FittedBox
, ווידג'ט שמתאים באופן אוטומטי את הרכיב שמוצב בתוכו לשטח הזמין בהתאם להגדרות שלכם.
אבל NavigationRail
לא מציג את התוויות אוטומטית כשיש מספיק מקום, כי הוא לא יכול לדעת מה נחשב למספיק מקום בכל הקשר. ההחלטה אם להשתמש ב-API הזה או לא נתונה בידיכם, המפתחים.
לדוגמה, אם רוצים להציג תוויות רק אם הרוחב של MyHomePage
הוא לפחות 600 פיקסלים.
הווידג'ט שבו צריך להשתמש במקרה הזה הוא LayoutBuilder
. הוא מאפשר לכם לשנות את עץ הווידג'טים בהתאם לנפח האחסון הזמין.
שוב, משתמשים בתפריט Refactor של Flutter ב-VS Code כדי לבצע את השינויים הנדרשים. אבל הפעם זה קצת יותר מסובך:
- בתוך השיטה
_MyHomePageState
שלbuild
, מציבים את הסמן עלScaffold
. - מציגים את התפריט Refactor (שינוי מבנה הקוד) באמצעות
Ctrl+.
(Windows/Linux) אוCmd+.
(Mac). - בוחרים באפשרות Wrap with Builder (הוספה ל-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
.
היתרון בהוספת דף מועדפים באופן עצמאי הוא שאתם לומדים יותר כשאתם מקבלים את ההחלטות בעצמכם. החיסרון הוא שאולי תיתקלו בבעיה שעדיין לא תוכלו לפתור בעצמכם. חשוב לזכור: זה בסדר להיכשל, וזה אחד מהמרכיבים החשובים ביותר בתהליך הלמידה. אף אחד לא מצפה שתצליחו לפתח באמצעות 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
- הפיכת האפליקציה לרספונסיבית
- איך יוצרים מראה ותחושה עקביים באפליקציה
מה עכשיו?
- מומלץ להתנסות באפליקציה שכתבתם במהלך שיעור ה-Lab הזה.
- כדי לראות איך אפשר להוסיף רשימות מונפשות, מעברי צבע, מעברי הדרגה ועוד, אפשר לעיין בקוד של הגרסה המתקדמת הזו של אותה אפליקציה.
- כדי לעקוב אחרי תהליך הלמידה, אפשר להיכנס לכתובת flutter.dev/learn.