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

1. מבוא

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

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

מה תלמדו

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

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

e9c6b402cd8003fd.png

ועכשיו, Filip ינחה אתכם לאורך כל הקודלאב!

לוחצים על 'הבא' כדי להתחיל את הסדנה.

2. הגדרת סביבת Flutter

הרשאת עריכה

כדי שהקודלאב הזה יהיה פשוט ככל האפשר, נניח שתשתמשו ב-Visual Studio Code‏ (VS Code) כסביבת הפיתוח. השירות זמין בחינם ופועל בכל הפלטפורמות העיקריות.

כמובן שאפשר להשתמש בכל עורך שרוצים: Android Studio, סביבות פיתוח משולבות (IDE) אחרות של IntelliJ, Emacs,‏ Vim או Notepad++. כולן פועלות עם Flutter.

מומלץ להשתמש ב-VS Code בקודלאב הזה, כי ההוראות מוגדרות כברירת מחדל למקשי קיצור ספציפיים ל-VS Code. קל יותר לומר 'לחצו כאן' או 'הקשה על המקש הזה' במקום 'צריך לבצע את הפעולה המתאימה בעורך כדי לבצע את הפעולה X'.

228c71510a8e868.png

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

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

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

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

16695777c07f18e5.png

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

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

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

התקנת Flutter

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

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

  1. Flutter SDK
  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'. בוחרים את הפקודה Flutter: New Project.

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

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

260a7d97f9678005.png

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

עכשיו נמחק את התוכן של 3 קבצים ומחליפים אותו בתוכנית בסיסית של האפליקציה.

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

בחלונית הימנית של 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.6.0

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.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

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),
        ],
      ),
    );

// ...

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

        ],
      ),
    );

// ...

כששומרים את השינוי, האפליקציה מתעדכנת שוב: מופיע לחצן, וכשלוחצים עליו, ב-Debug Console ב-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(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

הכיתה MyApp היא תת-מחלקה של StatelessWidget. ווידג'טים הם הרכיבים שמהם יוצרים כל אפליקציית Flutter. כפי שאתם רואים, גם האפליקציה עצמה היא ווידג'ט.

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

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

בשלב הבא, הכיתה MyAppState מגדירה את המצב של האפליקציה. זו הפעם הראשונה שאתם נכנסים ל-Flutter, ולכן נשתדל שהקודלאב יהיה פשוט וממוקד. יש הרבה דרכים יעילות לניהול מצב האפליקציה ב-Flutter. אחת הדרכים הקלות ביותר להסביר את הנושא היא ChangeNotifier, הגישה של האפליקציה הזו.

  • MyAppState מגדיר את הנתונים הנדרשים לצורך הפעולה של האפליקציה. בשלב הזה, הוא מכיל רק משתנה אחד עם שתי המילים האקראיות הנוכחיות. תוסיפו את הפרטים האלה בהמשך.
  • סוג המצב הוא תת-סוג של ChangeNotifier, כלומר הוא יכול להודיע לאחרים על השינויים שלו. לדוגמה, אם חל שינוי בזוג המילים הנוכחי, חלק מהווידג'טים באפליקציה צריכים לדעת על כך.
  • המצב נוצר ומסופק לכל האפליקציה באמצעות ChangeNotifierProvider (ראו את הקוד שלמעלה בקטע MyApp). כך כל ווידג'ט באפליקציה יכול לקבל את המצב. 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 בקודלאב הזה, אבל זה ווידג'ט שימושי שנמצא ברוב האפליקציות של Flutter בעולם האמיתי.
  4. Column הוא אחד מהווידג'טים הבסיסיים ביותר של פריסה ב-Flutter. הוא מקבל מספר כלשהו של צאצאים ומציב אותם בעמודה מלמעלה למטה. כברירת מחדל, העמודה מציגה את הצאצאים שלה בחלק העליון. בקרוב נשנה את זה כך שהעמודה תהיה במרכז.
  5. שיניתם את הווידג'ט Text בשלב הראשון.
  6. הווידג'ט השני של Text מקבל את appState ומקבל גישה לחבר היחיד בכיתה הזו, current (שהוא WordPair). ל-WordPair יש כמה פונקציות getter מועילות, כמו asPascalCase או asSnakeCase. כאן אנחנו משתמשים ב-asLowerCase, אבל אפשר לשנות את זה עכשיו אם מעדיפים אחת מהחלופות.
  7. שימו לב לשימוש הרב בפסיקים בסוף שורות בקוד 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. שיפור המראה של האפליקציה

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

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+. (ב-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….

כך אפשר לציין את הווידג'ט ההורה. מקלידים 'כרטיס' ומקישים על 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 הוא סגנון גדול שמיועד לטקסט בתצוגה. המילה display משמשת כאן במובן הטיפוגרפי, למשל גופן תצוגה. במסמכי העזרה של displayMedium כתוב ש "סגנונות תצוגה מיועדים לטקסט קצר וחשוב" – בדיוק התרחיש שלנו.
  • באופן תיאורטי, המאפיין displayMedium של העיצוב יכול להיות null. Dart, שפת התכנות שבה כותבים את האפליקציה הזו, היא שפה ללא סכנה של ערכים null, כך שלא תוכלו להפעיל שיטות של אובייקטים שעשויים להיות null. עם זאת, במקרה הזה אפשר להשתמש באופרטור ! ('אופרטור הקריאה') כדי להבטיח ל-Dart שאתם יודעים מה אתם עושים. (displayMedium הוא בהחלט לא null במקרה הזה. הסיבה לכך לא נכללת בקודלאב הזה).
  • קריאה ל-copyWith() ב-displayMedium מחזירה עותק של סגנון הטקסט עם השינויים שתגדירו. במקרה כזה, משנים רק את צבע הטקסט.
  • כדי לקבל את הצבע החדש, צריך לגשת שוב לנושא של האפליקציה. המאפיין onPrimary של ערכת הצבעים מגדיר צבע שמתאים לשימוש על הצבע הראשי של האפליקציה.

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

2405e9342d28c193.png

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

  • copyWith() מאפשר לשנות הרבה יותר מאפיינים של סגנון הטקסט, לא רק את הצבע. כדי לראות את הרשימה המלאה של המאפיינים שאפשר לשנות, מציבים את הסמן בכל מקום בתוך סוגרי הסוגריים של copyWith() ומקישים על Ctrl+Shift+Space (ב-Windows או ב-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}". באפשרות השנייה נעשה שימוש בהוספת מחרוזות (string interpolation) כדי ליצור מחרוזת (כמו "cheap head") משתי המילים שמכילה pair. שימוש בשתי מילים נפרדות במקום במילה מורכבת מבטיח שקוראי המסך יזהו אותן כראוי, ומספק חוויה טובה יותר למשתמשים עם לקויות ראייה.

עם זאת, מומלץ לשמור על הפשטות החזותית של pair.asLowerCase. משתמשים במאפיין semanticsLabel של Text כדי לשנות את התוכן החזותי של ווידג'ט הטקסט לתוכן סמנטי שמתאים יותר לקוראי מסך:

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        //  Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

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

מרכוז ממשק המשתמש

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

קודם כול, חשוב לזכור ש-BigCard הוא חלק מ-Column. כברירת מחדל, העמודות מקובצות עם הצאצאים שלהן בחלק העליון, אבל אפשר לשנות את ההגדרה הזו בקלות. עוברים לשיטה build() של MyHomePage ומבצעים את השינוי הבא:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  //  Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

כך הצאצאים מוצבים במרכז ה-Column לאורך הציר הראשי (האנכי) שלו.

b555d4c7f5000edf.png

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

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

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

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

455688d93c30d154.png

אם רוצים, אפשר לשנות את ההגדרה הזו עוד קצת.

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

// ...

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

3d53d2b071e2f372.png

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

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. עוברים ל-method‏ build() של MyHomePage, מעבירים את הסמן אל ElevatedButton, פותחים את התפריט Refactor באמצעות Ctrl+. או Cmd+. ובוחרים באפשרות Wrap with Row.

כששומרים, רואים ש-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 לשני ווידג'טים נפרדים.

בוחרים את כל 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 אחר. הפריסה שמתקבלת נראית בערך כך:

6bbda6c1835a1ae.png

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

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

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

בקרוב זה ישתנה.

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

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

e52d9c0937cc0823.jpeg

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

מזינים את StatefulWidget, סוג של ווידג'ט שיש לו State. קודם צריך להמיר את MyHomePage לווידג'ט עם שמירת מצב.

מציבים את הסמן בשורה הראשונה של MyHomePage (השורה שמתחילה ב-class MyHomePage...) ומפעילים את התפריט Refactor באמצעות Ctrl+. או Cmd+.. לאחר מכן בוחרים באפשרות Convert to 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(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

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

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

// ...

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

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

5685cf886047f6ec.png

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

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. בוחרים באפשרות Wrap with Builder ומקישים על Enter.
  4. משנים את השם של Builder החדש שנוסף ל-LayoutBuilder.
  5. משנים את רשימת הפרמטרים של הקריאה החוזרת מ-(context) ל-(context, constraints).

פונקציית ה-callback builder של LayoutBuilder נקראת בכל פעם שהאילוצים משתנים. המצב הזה יכול לקרות, למשל, במקרים הבאים:

  • המשתמש משנה את הגודל של חלון האפליקציה
  • המשתמש מסובב את הטלפון מפריסה לאורך לפריסה לרוחב, או להפך
  • הווידג'ט לצד MyHomePage גדל, והאילוצים של MyHomePage קטנים יותר
  • וכן הלאה

עכשיו הקוד יכול להחליט אם להציג את התווית על ידי שליחת שאילתה ל-constraints הנוכחי. מבצעים את השינוי הבא בשורה אחת בשיטה build של _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  //  Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

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

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

8. הוספת דף חדש

זוכרים את הווידג'ט Placeholder שהשתמשנו בו במקום בדף מועדפים?

הגיע הזמן לתקן את זה.

אם יש לכם רוח הרפתקנית, תוכלו לנסות לבצע את השלב הזה בעצמכם. המטרה היא להציג את רשימת favorites בווידג'ט חדש ללא מצב, FavoritesPage, ולאחר מכן להציג את הווידג'ט הזה במקום את Placeholder.

ריכזנו כאן כמה טיפים:

  • אם רוצים Column שאפשר לגלול בו, משתמשים בווידג'ט ListView.
  • חשוב לזכור: אפשר לגשת למכונה MyAppState מכל ווידג'ט באמצעות context.watch<MyAppState>().
  • אם רוצים לנסות גם ווידג'ט חדש, ל-ListTile יש מאפיינים כמו title (בדרך כלל לטקסט), leading (לסמלים או לדמויות) ו-onTap (לאינטראקציות). עם זאת, אפשר להשיג אפקטים דומים באמצעות הווידג'טים שכבר מוכרים לכם.
  • ב-Dart אפשר להשתמש בלולאות for בתוך ליברטיים של אוספים. לדוגמה, אם messages מכיל רשימה של מחרוזות, אפשר להשתמש בקוד כמו זה:

f0444bba08f205aa.png

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

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

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

מה הדבר הבא?

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