1. מבוא
Flutter היא ערכת הכלים של Google לפיתוח ממשקי משתמש. היא מאפשרת לפתח אפליקציות למכשירים ניידים, לאתרים ולמחשבים – מ-codebase יחיד. בשיעור ה-Codelab הזה תיצרו את אפליקציית Flutter הבאה:
האפליקציה יוצרת שמות שנשמעים מגניבים, כמו newstay, lightstream, mainbrake או graypine. המשתמש יכול לבקש את השם הבא, להוסיף את השם הנוכחי למועדפים ולבדוק את רשימת המועדפים בדף נפרד. האפליקציה רספונסיבית לגדלים שונים של מסכים.
מה תלמדו
- העקרונות הבסיסיים של Flutter
- יצירת פריסות ב-Flutter
- חיבור בין אינטראקציות של משתמשים (כמו לחיצות על לחצנים) לבין התנהגות האפליקציה
- שמירה על סדר בקוד Flutter
- התאמת האפליקציה למסכים שונים
- איך יוצרים מראה ותחושה עקביים באפליקציה
נתחיל עם מסגרת בסיסית כדי שתוכלו לדלג ישר לחלקים המעניינים.

והנה פיליפ מסביר על כל ה-codelab!
לוחצים על 'הבא' כדי להתחיל את המעבדה.
2. הגדרת סביבת Flutter
עריכה
כדי ששיעור ה-Codelab הזה יהיה פשוט ככל האפשר, אנחנו מניחים שתשתמשו ב-Visual Studio Code (VS Code) כסביבת הפיתוח שלכם. השירות זמין בחינם ופועל בכל הפלטפורמות העיקריות.
כמובן שאפשר להשתמש בכל עורך שרוצים: Android Studio, סביבות פיתוח משולבות אחרות של IntelliJ, 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: טעינה חמה של מצב. אי אפשר לבצע טעינה מחדש מהירה של אפליקציות אינטרנט ב-Flutter.
צריך לבחור עכשיו. חשוב לזכור: תמיד אפשר להפעיל את האפליקציה במערכות הפעלה אחרות בהמשך. פשוט, כשמגדירים מראש את היעד להתפתחות, השלב הבא מתבצע בצורה חלקה יותר.
התקנת Flutter
ההוראות העדכניות ביותר להתקנת Flutter SDK זמינות תמיד בכתובת docs.flutter.dev.
ההוראות באתר Flutter כוללות לא רק את ההתקנה של ה-SDK עצמו, אלא גם את הכלים שקשורים ליעד הפיתוח ואת התוספים לעורך. חשוב לזכור שבקוד לאב הזה צריך להתקין רק את הרכיבים הבאים:
- Flutter SDK
- Visual Studio Code עם הפלאגין Flutter
- התוכנה שנדרשת ליעד הפיתוח שבחרתם (לדוגמה: Visual Studio לטירגוט Windows או Xcode לטירגוט macOS)
בקטע הבא נסביר איך ליצור את פרויקט Flutter הראשון.
אם נתקלת בבעיות עד עכשיו, יכול להיות שחלק מהשאלות והתשובות האלה (מ-StackOverflow) יעזרו לך לפתור אותן.
שאלות נפוצות
- איך מוצאים את הנתיב של Flutter SDK?
- מה עושים אם הפקודה Flutter לא נמצאה?
- איך פותרים את הבעיה 'המתנה לפקודת 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),
],
),
);
// ...
שימו לב שהאפליקציה משתנה מיד, אבל המילה האקראית נשארת ללא שינוי. זהו הטעינה החמה עם שמירת המצב המפורסמת של Flutter בפעולה. Hot Reload מופעל כששומרים שינויים בקובץ מקור.
שאלות נפוצות
- מה קורה אם התכונה 'טעינה חמה' לא פועלת ב-VSCode?
- האם צריך ללחוץ על r כדי לבצע Hot Reload ב-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 מופיעה ההודעה 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 מרחיבה את 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. בשיעור Codelab הזה לא תעבדו עםScaffold, אבל זה ווידג'ט שימושי שמופיע ברוב האפליקציות של 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 ואת ה-method build() בתוכה. כמו קודם, מציגים את התפריט Refactor (שינוי מבנה הקוד) בווידג'ט Text. אבל הפעם לא תחלצו את הווידג'ט.
במקום זאת, בוחרים באפשרות הוספת שוליים מסביב. הפעולה הזו יוצרת ווידג'ט אב חדש מסביב לווידג'ט Text בשם Padding. אחרי השמירה, אפשר לראות שהמילה האקראית כבר לא צפופה.
הגדלת השוליים הפנימיים מערך ברירת המחדל 8.0. לדוגמה, אפשר להשתמש בערך כמו 20 כדי להגדיל את הריווח הפנימי.
אחר כך, עולים רמה אחת. מציבים את הסמן על הווידג'ט Padding, פותחים את התפריט Refactor (שינוי מבנה הקוד) ובוחרים באפשרות Wrap with widget... (הוספת ווידג'ט מסוג Wrap).
כך אפשר לציין את הווידג'ט הראשי. מקלידים Card (כרטיס) ומקישים על Enter.
הווידג'ט Padding, ולכן גם Text, עטוף בווידג'ט Card.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
האפליקציה תיראה עכשיו בערך כך:

עיצוב וסגנון
כדי שהכרטיס יבלוט יותר, צובעים אותו בצבע עשיר יותר. כדאי לשמור על ערכת צבעים עקבית, ולכן מומלץ להשתמש בTheme של האפליקציה כדי לבחור את הצבע.
מבצעים את השינויים הבאים בשיטה build() של BigCard.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
שתי השורות החדשות האלה מבצעות הרבה עבודה:
- קודם, הקוד מבקש את העיצוב הנוכחי של האפליקציה באמצעות
Theme.of(context). - לאחר מכן, הקוד מגדיר את צבע הכרטיס כך שיהיה זהה למאפיין
colorSchemeשל ערכת הנושא. ערכת הצבעים מכילה הרבה צבעים, והצבעprimaryהוא הצבע הבולט ביותר שמגדיר את האפליקציה.
הכרטיס צבוע עכשיו בצבע הראשי של האפליקציה:

אפשר לשנות את הצבע הזה ואת ערכת הצבעים של כל האפליקציה. כדי לעשות את זה, גוללים למעלה אל MyApp ומשנים שם את צבע הבסיס של ColorScheme.
שימו לב לאנימציה החלקה של הצבע. זה נקרא אנימציה מרומזת. רכיבי ווידג'ט רבים ב-Flutter מבצעים אינטרפולציה חלקה בין ערכים, כך שממשק המשתמש לא פשוט "קופץ" בין מצבים.
גם הצבע של הלחצן המוגבה שמתחת לכרטיס משתנה. זה היתרון בשימוש ב-Theme ברמת האפליקציה, לעומת כתיבת ערכים בתוך הקוד.
TextTheme
עדיין יש בעיה בכרטיס: הטקסט קטן מדי והצבע שלו מקשה על הקריאה. כדי לפתור את הבעיה, מבצעים את השינויים הבאים בשיטה build() של BigCard.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
מה הסיבה לשינוי הזה:
- השימוש ב
theme.textThemeמאפשר לכם לגשת לעיצוב הגופן של האפליקציה. המחלקות האלה כוללות חברים כמוbodyMedium(לטקסט רגיל בגודל בינוני),caption(לכתוביות של תמונות) אוheadlineLarge(לכותרות גדולות). - המאפיין
displayMediumהוא סגנון גדול שנועד להצגת טקסט. המילה תצוגה משמשת כאן במובן הטיפוגרפי, כמו בגופן לתצוגה. במסמכי התיעוד שלdisplayMediumמצוין ש "סגנונות תצוגה שמורים לטקסט קצר וחשוב" – בדיוק מה שאנחנו צריכים. - המאפיין
displayMediumשל העיצוב יכול להיות באופן תיאורטיnull. שפת התכנות Dart, שבה אתם כותבים את האפליקציה הזו, היא null-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. כברירת מחדל, העמודות מקבצות את הנכסים המשניים שלהן בחלק העליון, אבל אפשר לשנות את זה. עוברים לשיטה build() של MyHomePage ומבצעים את השינוי הבא:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
הפעולה הזו ממקמת את רכיבי הילד במרכז Column לאורך הציר הראשי (האנכי).

האלמנטים הצאצאים כבר ממוקמים במרכז לאורך ציר הרוחב של העמודה (כלומר, הם כבר ממוקמים במרכז באופן אופקי). אבל Column התוכן עצמו לא ממוקם במרכז בתוך Scaffold. אפשר לבדוק את זה באמצעות הכלי לבדיקת ווידג'טים.
השימוש ב-Widget Inspector הוא מעבר להיקף של ה-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>[], באמצעות generics. כך האפליקציה תהיה חזקה יותר – 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+.. אחר כך בוחרים באפשרות המרה ל-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שהיה שם עד עכשיו. - כשקוראים לפונקציית הקריאה החוזרת
onDestinationSelected, במקום להדפיס את הערך החדש במסוף, מקצים אותו ל-selectedIndexבתוך קריאה ל-setState(). הקריאה הזו דומה לשיטתnotifyListeners()ששימשה בעבר – היא מוודאת שממשק המשתמש מתעדכן.
סרגל הניווט מגיב עכשיו לאינטראקציה של המשתמש. אבל האזור המורחב בצד שמאל נשאר ללא שינוי. הסיבה לכך היא שהקוד לא משתמש ב-selectedIndex כדי לקבוע איזה מסך יוצג.
שימוש ב-selectedIndex
ממקמים את הקוד הבא בחלק העליון של השיטה build של _MyHomePageState, ממש לפני return Scaffold:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
בודקים את קטע הקוד הזה:
- הקוד מכריז על משתנה חדש,
page, מהסוגWidget. - לאחר מכן, משפט switch מקצה מסך ל-
page, בהתאם לערך הנוכחי ב-selectedIndex. - מכיוון שעדיין אין
FavoritesPage, אפשר להשתמש ב-Placeholder– ווידג'ט שימושי שמצייר מלבן עם קו חוצה בכל מקום שבו מציבים אותו, כדי לסמן את החלק הזה בממשק המשתמש כלא גמור.

- בהתאם לעיקרון 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 לא מציג את התוויות באופן אוטומטי כשיש מספיק מקום, כי הוא לא יכול לדעת מה נחשב למספיק מקום בכל הקשר. ההחלטה אם להשתמש ב-SDK הזה או לא נתונה בידיכם, המפתחים.
נניח שאתם מחליטים להציג תוויות רק אם הרוחב של MyHomePage הוא לפחות 600 פיקסלים.
הווידג'ט שבו צריך להשתמש במקרה הזה הוא LayoutBuilder. הוא מאפשר לכם לשנות את עץ הווידג'טים בהתאם לכמות השטח הפנוי שיש לכם.
שוב, משתמשים בתפריט Refactor של Flutter ב-VS Code כדי לבצע את השינויים הנדרשים. אבל הפעם זה קצת יותר מסובך:
- בשיטה
buildשל_MyHomePageState, מציבים את הסמן עלScaffold. - מציגים את התפריט Refactor (שינוי מבנה הקוד) באמצעות
Ctrl+.(Windows/Linux) אוCmd+.(Mac). - בוחרים באפשרות Wrap with 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.