אפליקציית Flutter הראשונה שלך

1. מבוא

Flutter היא ערכת הכלים לבניית ממשק משתמש של Google, שנועדה לבנות אפליקציות לנייד, לאינטרנט ולמחשב על בסיס קוד יחיד. ב-Codelab הזה, תפתחו את אפליקציית Flutter הבאה:

האפליקציה יוצרת שמות שנשמעים קרירים כמו newstay, lightstream, mainbrake או graypine. המשתמש יכול לבקש את השם הבא, להוסיף את השם הנוכחי למועדפים ולבדוק את רשימת השמות המועדפים בדף נפרד. האפליקציה מגיבה לגדלים שונים של מסכים.

מה תלמדו

  • העקרונות הבסיסיים של Flutter
  • יצירת פריסות ב-Flutter
  • קישור בין אינטראקציות של משתמשים (כמו לחיצות על לחצנים) להתנהגות באפליקציה
  • שמירה על ארגון הקוד של Flutter
  • איך להפוך את האפליקציה לרספונסיבית (למסכים שונים)
  • השגת מראה עקבי של האפליקציה

תתחילו עם פיסול בסיסי כדי שתוכלו לקפוץ ישירות לחלקים המעניינים.

e9c6b402cd8003fd.png

והנה פיליפ שידריך אתכם בכל ה-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".

228c71510a8e868.png

בחירת יעד פיתוח

Flutter היא ערכת כלים מרובת פלטפורמות. האפליקציה יכולה לפעול בכל אחת ממערכות ההפעלה הבאות:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • אינטרנט

עם זאת, מקובל לבחור מערכת הפעלה אחת שתפתחו בעיקר. זהו 'יעד הפיתוח' שלכם – מערכת ההפעלה שבה האפליקציה פועלת במהלך הפיתוח.

16695777c07f18e5.png

לדוגמה, נניח שאתם משתמשים במחשב נייד עם Windows כדי לפתח אפליקציית Flutter. אם בוחרים ב-Android כיעד הפיתוח, בדרך כלל מחברים מכשיר Android למחשב הנייד עם Windows באמצעות כבל USB, והאפליקציה בשלב הפיתוח פועלת במכשיר Android המחובר הזה. אבל אפשר גם לבחור ב-Windows כיעד הפיתוח, כלומר האפליקציה בשלב הפיתוח פועלת כאפליקציה ל-Windows לצד העורך.

מפתה לבחור את האינטרנט כיעד הפיתוח שלכם. החיסרון של הבחירה הזו הוא אובדן של אחת מתכונות הפיתוח השימושיות ביותר של Flutter: 'טעינה חמה של מצב' (Stateful Reload). Flutter לא יכולה לטעון מחדש אפליקציות אינטרנט במהירות.

יש לך אפשרות לבחור עכשיו. חשוב לזכור: תמיד אפשר להריץ את האפליקציה במערכות הפעלה אחרות בשלב מאוחר יותר. אם תקפידו על יעד פיתוח ברור, השלב הבא יהיה חלק יותר.

להתקנת Flutter

ההוראות העדכניות ביותר להתקנה של Flutter SDK תמיד זמינות בכתובת docs.flutter.dev.

ההוראות באתר Flutter כוללות לא רק את ההתקנה של ה-SDK עצמה, אלא גם את הכלים הקשורים ליעד הפיתוח ואת יישומי הפלאגין של העריכה. חשוב לזכור שב-Codelab הזה צריך להתקין רק את הדברים הבאים:

  1. SDK של Flutter
  2. Visual Studio Code עם הפלאגין Flutter
  3. התוכנה הנדרשת על ידי יעד הפיתוח שבחרתם (לדוגמה: Visual Studio כדי לטרגט ל-Windows, או Xcode כדי לטרגט macOS)

בקטע הבא תיצרו את פרויקט Flutter הראשון שלכם.

אם נתקלתם בבעיות עד עכשיו, יכול להיות שחלק מהשאלות והתשובות האלה (מ-StackOverflow) יעזרו לכם לפתור את הבעיה.

שאלות נפוצות

3. יצירת פרויקט

יוצרים את הפרויקט הראשון של Flutter

מפעילים את Visual Studio Code ופותחים את לוח הפקודות (באמצעות F1 או Ctrl+Shift+P או Shift+Cmd+P). מתחילים להקליד 'שטף חדש'. בוחרים בפקודה Flutter: New Project.

בשלב הבא בוחרים ב-Application ואז בתיקייה שבה תיצרו את הפרויקט. זו יכולה להיות ספריית הבית שלך, או משהו כמו C:\src\.

לסיום, נותנים שם לפרויקט. משהו כמו namer_app או my_awesome_namer.

260a7d97f9678005.png

עכשיו Flutter יוצרת את תיקיית הפרויקט שלך ו-VS Code פותח אותה.

עכשיו התוכן של שלושה קבצים יוחלף בפיגום בסיסי של האפליקציה.

העתקה & הדבקת האפליקציה הראשונית

בחלונית השמאלית של VS Code, מוודאים שהאפשרות Explorer מסומנת ופותחים את הקובץ pubspec.yaml.

e2a5bab0be07f4f7.png

החלפת התוכן של הקובץ הזה בערך הבא:

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.

a781f218093be8e0.png

צריך להחליף את התוכן שלו בערך הבא:

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/.

e54c671c9bb4d23d.png

החלפת התוכן של הקובץ הזה בערך הבא:

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 פתוחה, מחפשים את האפשרות 'הפעלה' b0a5d0200af5985d.png בפינה השמאלית העליונה של החלון של VS Code, ולוחצים עליו.

לאחר כדקה, האפליקציה תופעל במצב ניפוי באגים. זה עדיין לא נראה כמו משהו:

f96e7dfb0937d7f4.png

טעינה מחדש חמה ראשונה

בחלק התחתון של 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. טעינה מחדש חמה מופעלת כששומרים שינויים בקובץ מקור.

שאלות נפוצות

הוספת לחצן

בשלב הבא צריך להוסיף לחצן בתחתית ה-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). כך כל ווידג'ט באפליקציה יכול לשמור על מצב המדינה. d9b6ecac5494a6ff.png

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, הווידג'ט שכבר שיניתם. כל שורה ממוספרת שמתחת להערה ממופה להערה עם מספר שורה בקוד שלמעלה:

  1. בכל ווידג'ט מוגדרת שיטת build() שמופעלת באופן אוטומטי בכל פעם שהנסיבות של הווידג'ט משתנות, כך שהווידג'ט תמיד מעודכן.
  2. MyHomePage עוקב אחרי שינויים במצב הנוכחי של האפליקציה באמצעות השיטה watch.
  3. כל שיטה של build חייבת להחזיר ווידג'ט או (בדרך כלל) עץ בתוך ווידג'טים. במקרה הזה, הווידג'ט ברמה העליונה הוא Scaffold. אתם לא מתכוונים לעבוד עם Scaffold ב-Codelab הזה, אבל הוא ווידג'ט מועיל שנמצא ברוב האפליקציות של Flutter בעולם האמיתי.
  4. Column הוא אחד מהווידג'טים הבסיסיים ביותר של הפריסה ב-Flutter. היא לוקחת כל מספר של צאצאים ומציבה אותם בעמודה מלמעלה למטה. כברירת מחדל, העמודה מציבה את הצאצאים שלה בחלק העליון של הדף. בקרוב משנים את המיקום כך שהעמודה תהיה במרכז.
  5. שינית את הווידג'ט הזה של Text בשלב הראשון.
  6. הווידג'ט השני של Text מקבל appState, והוא ניגש לחבר היחיד בכיתה, current (כלומר, WordPair). WordPair יש כמה רעיונות שימושיים, כמו asPascalCase או asSnakeCase. כאן אנחנו משתמשים בasLowerCase, אבל אפשר לשנות את ההגדרה הזו עכשיו אם ברצונך להשתמש באחת מהחלופות.
  7. שימו לב שבקוד של 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. אפליקציה יפה יותר

כך נראית האפליקציה כרגע.

3dd8a9d8653bdc56.png

לא משהו. החלק המרכזי של האפליקציה – צמד המילים שנוצר באופן אקראי – צריך להיות גלוי יותר. אחרי הכול, זו הסיבה העיקרית לכך שהמשתמשים שלנו משתמשים באפליקציה הזו! בנוסף, תוכן האפליקציה לא ממורכז באופן מוזר, וכל האפליקציה שחורה משעממת לבן.

הקטע הזה עוסק בבעיות האלה בעיצוב של האפליקציה. המטרה הסופית של הקטע הזה היא בערך כך:

2bbee054d81a3127.png

חילוץ ווידג'ט

השורה שאחראית להצגת צמד המילים הנוכחי נראה כך: 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, אפשר לעשות זאת באחת משתי הדרכים הבאות:

  1. לוחצים לחיצה ימנית על קטע הקוד שרוצים לשנות את הקוד (Text במקרה הזה) ובוחרים באפשרות Refactor... מהתפריט הנפתח.

או

  1. מעבירים את הסמן לקטע הקוד שרוצים להגדיר מחדש (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.

6031adbc0a11e16b.png

עיצוב וסגנון

כדי להבליט את הכרטיס יותר, צובעים אותו בצבע עשיר יותר. מכיוון שתמיד כדאי לשמור על ערכת צבעים עקבית, אפשר להשתמש ב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 הוא הצבע הבולט ביותר שמגדיר את האפליקציה.

עכשיו הכרטיס נצבע בצבע הראשי של האפליקציה:

a136f7682c204ea1.png

אפשר לשנות את הצבע הזה ואת ערכת הצבעים של האפליקציה כולה על ידי גלילה למעלה אל 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 של סכימת הצבעים מגדיר צבע שמתאים לשימוש על הצבע הראשי של האפליקציה.

עכשיו האפליקציה אמורה להיראות כך:

2405e9342d28c193.png

אם אתה רוצה, ניתן לשנות את פרטי הכרטיס. רעיונות לשינויים:

  • ב-copyWith() אפשר לשנות הרבה יותר את סגנון הטקסט ולא רק את הצבע. כדי לקבל את רשימת המאפיינים המלאה שניתן לשנות, מציבים את הסמן במקום כלשהו בתוך הסוגריים של copyWith() ומקישים על Ctrl+Shift+Space (Win/Linux) או Cmd+Shift+Space (Mac).
  • באופן דומה, ניתן לשנות עוד פרטים בווידג'ט Card. לדוגמה, ניתן להגדיל את הצל של הכרטיס על ידי הגדלת הערך של הפרמטר elevation.
  • כדאי להתנסות בצבעים. מלבד theme.colorScheme.primary, יש גם את .secondary, .surface ומבחר עצום של אחרים. לכל הצבעים האלה יש onPrimary מקבילים.

שיפור הנגישות

Flutter הופכת אפליקציות לנגישות כברירת מחדל. לדוגמה, כל אפליקציה של Flutter מציגה בצורה נכונה את כל הטקסט והרכיבים האינטראקטיביים באפליקציה לקוראי מסך כמו TalkBack ו-VoiceOver.

d1fad7944fb890ea.png

עם זאת, לפעמים נדרשת עבודה רבה. במקרה של האפליקציה הזו, יכול להיות שלקורא המסך יהיו בעיות בהגייה של חלק מצמדי המילים שנוצרו. לבני אדם אין בעיות בזיהוי שתי המילים ב-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 לאורך הציר הראשי (האנכי) שלו.

b555d4c7f5000edf.png

הצאצאים כבר מופיעים במרכז לאורך ציר חצי העמודה (במילים אחרות, הם כבר ממרכזים לרוחב). אבל התמונה Column עצמה לא נמצאת במרכז בתוך Scaffold. נוכל לבדוק את זה באמצעות הכלי לבדיקת ווידג'טים.

הכלי לבדיקת ווידג'טים חורג מההיקף של ה-Codelab הזה, אבל אפשר לראות שכאשר ה-Column מודגש, הוא לא תופס את כל רוחב האפליקציה. היא תופסת שטח אופקי רק ככל שהילדים שלה צריכים.

אפשר פשוט למרכז את העמודה עצמה. מציבים את הסמן על Column, פותחים את התפריט Refactoring (עם Ctrl+. או Cmd+.) ובוחרים באפשרות גלישת טקסט באמצעות המרכז.

עכשיו האפליקציה אמורה להיראות כך:

455688d93c30d154.png

אם רוצים, אפשר לשנות קצת יותר.

  • אפשר להסיר את הווידג'ט 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'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

והאפליקציה נראית כך:

3d53d2b071e2f372.png

בקטע הבא נוסיף את האפשרות לסמן מילים כ'מועדפים' (או לסמן 'לייק') על מילים שנוצרו על ידי AI.

6. הוספת פונקציונליות

האפליקציה עובדת ומדי פעם יש בה צמדי מילים מעניינים. עם זאת, בכל פעם שמשתמש לוחץ על הבא, כל צמד מילים נעלם באופן סופי. עדיף להשתמש באפשרות של 'להיזכר' את ההצעות הכי טובות: למשל 'לייק' לחצן.

e6b01a8c90df8ffa.png

הוספת הלוגיקה העסקית

גוללים אל 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'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

ממשק המשתמש חזר למקום שבו היה קודם.

3d53d2b071e2f372.png

אחר כך, צריך להוסיף את הלחצן לייק ולחבר אותו אל toggleFavorite(). אם יש לכם אתגר, נסו קודם לעשות זאת בעצמכם, בלי לבדוק את בלוק הקוד שבהמשך.

e6b01a8c90df8ffa.png

זה בסדר אם לא עושים את זה בדיוק באותו האופן כמו שמתואר בהמשך. למעשה, אל תדאגו בקשר לסמל הלב, אלא אם אתם ממש רוצים אתגר משמעותי.

זה בסדר לגמרי להיכשל – אחרי הכול, זו השעה הראשונה שלכם עם Flutter.

252f7c4a212c94d2.png

הנה דרך אחת להוסיף את הלחצן השני ל-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 הראשונים שלך.

f62c54f5401a187.png

כדי להשלים את השלב הזה בהקדם האפשרי, צריך לפצל את 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'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

אחרי השמירה, הצד החזותי של ממשק המשתמש מוכן, אבל הוא לא עובד. לחיצה על ♥︎ (הלב) ברכבת הניווט לא תגרום לשום פעולה.

388bc25fe198c54a.png

בודקים את השינויים.

  • קודם כל, חשוב לשים לב שכל התוכן של MyHomePage מחולץ לווידג'ט חדש, GeneratorPage. החלק היחיד בווידג'ט הישן של MyHomePage שלא חולץ הוא Scaffold.
  • הקובץ MyHomePage החדש מכיל Row עם שני צאצאים. הווידג'ט הראשון הוא SafeArea והשני הוא ווידג'ט Expanded.
  • SafeArea מוודא הצאצא שלו לא מוסתר על ידי חריץ חומרה או שורת סטטוס. באפליקציה הזו, הווידג'ט מופיע סביב NavigationRail כדי למנוע, לדוגמה, את הסתרת לחצני הניווט על ידי שורת הסטטוס של הנייד.
  • אפשר לשנות את הקו extended: false ב-NavigationRail ל-true. כאן יוצגו התוויות ליד הסמלים. בשלב הבא נסביר איך לעשות את זה באופן אוטומטי כשלאפליקציה יש מספיק שטח אופקי.
  • ברכבת הניווט יש שני יעדים (דף הבית ומועדפים), עם הסמלים והתוויות המתאימים. הוא גם מגדיר את selectedIndex הנוכחי. אינדקס אפס שנבחר בוחר את היעד הראשון, אינדקס נבחר של יעד אחד בוחר את היעד השני וכן הלאה. בינתיים, הקוד הוא אפס.
  • ברכבת הניווט מוגדר גם מה קורה כשהמשתמש בוחר אחד מהיעדים עם onDestinationSelected. בשלב זה, האפליקציה רק מפיקה את ערך האינדקס המבוקש באמצעות print().
  • הצאצא השני של Row הוא הווידג'ט Expanded. ווידג'טים מורחבים שימושיים במיוחד בשורות ובעמודות — הם מאפשרים ליצור פריסות שבהן חלק מהילדים תופסים רק את השטח הנדרש (SafeArea, במקרה הזה) וווידג'טים אחרים צריכים לתפוס כמה שיותר מקום (Expanded, במקרה הזה). אחת הדרכים לחשוב על ווידג'טים של Expanded היא שהם 'חמדניים'. כדי להבין טוב יותר את התפקיד של הווידג'ט הזה, אפשר לעטוף את הווידג'ט SafeArea עם פרמטר Expanded אחר. הפריסה שמתקבלת נראית בערך כך:

6bda6c1835a1ae.png

  • שני ווידג'טים של Expanded מפצלים את כל השטח האופקי הזמין ביניהם, למרות שפרוסה קטנה בצד שמאל של סרגל הניווט.
  • בתוך הווידג'ט Expanded יש Container צבעוני, ובתוך המאגר יש את GeneratorPage.

ווידג'טים ללא מצב לעומת ווידג'טים עם שמירת מצב

עד עכשיו, MyAppState מכסה את כל הצרכים של המדינה שלך. לכן, כל הווידג'טים שכתבת עד עכשיו חסרים מצב. הם לא מכילים מצב שניתן לשינוי משל עצמם. אף אחד מהווידג'טים לא יכול לשנות את עצמו — הוא צריך לעבור דרך MyAppState.

זה עומד להשתנות.

נדרשת דרך כלשהי כדי לשמור על הערך של selectedIndex של רכבת הניווט. מומלץ גם לשנות את הערך הזה מתוך הקריאה החוזרת (callback) של onDestinationSelected.

ניתן להוסיף את selectedIndex בתור עוד נכס של MyAppState. וזה יעבוד. אבל אפשר לדמיין שמצב האפליקציה יתארך במהירות מעבר לסיבה, אם כל ווידג'ט ישמור בו את הערכים שלו.

e52d9c0937cc0823.jpeg

חלק מהמצבים האלה רלוונטיים רק לווידג'ט אחד, לכן הוא צריך להישאר עם אותו ווידג'ט.

צריך להזין את 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(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

בודקים את השינויים:

  1. צריך להוסיף משתנה חדש, selectedIndex, ולאתחל אותו עם 0.
  2. אתם משתמשים במשתנה החדש הזה בהגדרה NavigationRail במקום במשתנה 0 שהיה בתוך הקוד עד עכשיו.
  3. כשמתבצעת קריאה לקריאה החוזרת של 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');
}

// ...

בודקים את קטע הקוד הזה:

  1. הקוד מצהיר על משתנה חדש, page, מסוג Widget.
  2. לאחר מכן, הצהרת החלפה מקצה מסך ל-page, בהתאם לערך הנוכחי ב-selectedIndex.
  3. מכיוון שאין עדיין FavoritesPage, צריך להשתמש ב-Placeholder. ווידג'ט שימושי שמסמן מלבן צלב בכל מקום שבו מציבים אותו, ומסמן את החלק הזה בממשק המשתמש כלא גמור.

5685cf886047f6ec.png

  1. אם מחילים את עקרון הכישלון המהיר, גם הצהרת ההחלפה מוודאת להקפיץ הודעת שגיאה אם הערך של 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) כשיש מספיק מקום לתמונות.

a8873894c32e0d0b.png

Flutter מספקת כמה ווידג'טים שעוזרים להפוך את האפליקציות לרספונסיביות באופן אוטומטי. לדוגמה, Wrap הוא ווידג'ט שדומה ל-Row או ל-Column שעוטף ילדים באופן אוטומטי ל'שורה' הבאה (שנקרא 'הרצה') כשאין מספיק שטח אנכי או אופקי. יש את FittedBox, ווידג'ט שמתאים אוטומטית לילד או לילדה בשטח הזמין לפי המפרט שלכם.

עם זאת, NavigationRail לא מציג תוויות באופן אוטומטי כשיש מספיק מקום כי אין לו אפשרות לדעת מה יש מספיק מקום בכל הקשר. באחריותך, המפתח, לבצע את השיחה.

נניח שמחליטים להציג תוויות רק אם הרוחב של MyHomePage הוא לפחות 600 פיקסלים.

הווידג'ט שבו צריך להשתמש, במקרה הזה, הוא LayoutBuilder. ניתן לשנות את עץ הווידג'ט בהתאם לנפח האחסון הזמין.

שוב, משתמשים בתפריט Refactor של Flutter ב-VS Code כדי לבצע את השינויים הנדרשים. הפעם זה קצת יותר מורכב:

  1. בתוך השיטה build של _MyHomePageState, מציבים את הסמן על Scaffold.
  2. קוראים לתפריט Refactor דרך התפריט Ctrl+. (Windows/Linux) או Cmd+. (Mac).
  3. בוחרים באפשרות גלישה עם Builder ומקישים על Enter.
  4. שינוי השם של האפליקציה Builder החדשה שהוספת אל LayoutBuilder.
  5. שינוי רשימת הפרמטרים של קריאה חוזרת מ-(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 מכיל רשימת מחרוזות, אפשר להשתמש בקוד כמו:

f0444bba08f205aa.png

לעומת זאת, אם יש לך ניסיון בתכנות פונקציונלי יותר, אם הדרך שבה אפשר להשתמש ב-Dart יכולה לכתוב קוד כמו messages.map((m) => Text(m)).toList(). כמובן שתמיד אפשר ליצור רשימה של ווידג'טים וחובה להוסיף אותם באמצעות השיטה build.

היתרון של הוספת הדף מועדפים הוא קבלת מידע נוסף באמצעות קבלת החלטות משלכם. החיסרון הוא מצב שבו אתם עלולים להיתקל בבעיות שאתם עדיין לא מצליחים לפתור בעצמכם. זכרו: להיכשל הוא בסדר, והוא אחד מרכיבי הלמידה החשובים ביותר בלמידה. אף אחד לא מצפה ממך לפתח ציפורניים בשעה הראשונה, וגם לא לך.

252f7c4a212c94d2.png

השלבים הבאים הם רק דרך אחת להטמיע את דף המועדפים. אופן ההטמעה של הקוד (אני מקווה) יעודד אתכם לשחק עם הקוד – יעזור לכם לשפר את ממשק המשתמש ולהפוך אותו לאישי.

הנה הכיתה החדשה ב-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, והפכת אותו לאפליקציה קטנה ומהנה ורספונסיבית.

d6e3d5f736411f13.png

הנושאים שטיפלנו בהם

  • העקרונות הבסיסיים של Flutter
  • יצירת פריסות ב-Flutter
  • קישור בין אינטראקציות של משתמשים (כמו לחיצות על לחצנים) להתנהגות באפליקציה
  • שמירה על ארגון הקוד של Flutter
  • זה הזמן להגדיר את האפליקציה רספונסיבית
  • השגת מראה עקבי של האפליקציה

מה הדבר הבא?

  • כדאי להתנסות באפליקציה שכתבת במהלך שיעור ה-Lab הזה.
  • בודקים את הקוד של הגרסה המתקדמת הזו של אותה האפליקציה כדי לראות איך להוסיף רשימות עם אנימציה, הדרגתיות, עמעום הדרגתי ועוד.
  • כדי לעקוב אחר תהליך הלמידה, אפשר להיכנס לכתובת flutter.dev/learn.