שלום Flutter World

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

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

1. התקנות וכו

הוראות התקנה מפורטות נמצאות באתר שלהם. מכל המערכות ההתקנה הכי קלה היא דווקא על לינוקס - יש פשוט קובץ snap להתקנה, אבל גם על חלונות ומק ההתקנה לא אמורה לקחת יותר מכמה דקות של העתקת שורות מהמדריך. כאן נמצאים מדריכי ההתקנה לכל שלושת מערכות ההפעלה: https://flutter.dev/docs/get-started/install.

אחרי זה יש גם דף שמסביר איך לקנפג את עורך הטקסט ל-4 עורכים פופולריים (אימקס, VS Code, IntelliJ ו Android Studio): https://flutter.dev/docs/get-started/editor?tab=vscode

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

אחרי ההתקנה פלאטר מגיעה עם Project Generator ובשביל ליצור תבנית של פרויקט חדש נכנסים לתיקיה ריקה ומפעילים:

$ flutter create .

2. מבנה התוכנית

יצרן הפרויקטים בנה לנו תיקיה ספציפית לכל מערכת הפעלה (עם השמות android, ios, linux, macos, windows ו web) ותיקיה בשם lib בה נמצא הקוד המשותף. בתוך התיקיות הספציפיות נכתוב קוד ספציפי לכל מערכת הפעלה - לדוגמה קוד שחושף איזשהו Native API, ובתוך תיקיית lib יהיה הקוד המשותף לכל המערכות.

בשביל הדוגמה אני רוצה לבנות אפליקציית Desktop פשוטה להמרת יחידות זמן: האפליקציה מראה 3 תיבות טקסט, אחת בשביל שניות, השניה בשביל הדקות והשלישית בשביל השעות. שינוי הערך בכל תיבה יגרום לעדכון הערך בשתי התיבות האחרות בהתאמה. כלומר אם כתבתי 7200 בתיבת השניות יופיע לי 2 בתיבת השעות ו 120 בתיבת הדקות. אם כתבתי 1 בתיבת השעות אוטומטית יופיע 3600 בתיבת השניות ו 60 בתיבת הדקות.

כל הקוד הרלוונטי נכנס ב 90 שורות בקובץ lib/main.dart. הנה הדבקה שלו ואחר כך כמה הערות שיעזרו להבנה:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final secondsController = TextEditingController();
  final minutesController = TextEditingController();
  final hoursController = TextEditingController();

  setTime(String value, int factor) {
    try {
      double seconds = double.parse(value) * factor;
      String secondsText = seconds.toStringAsFixed(2);
      String hoursText = (seconds / 3600).toStringAsFixed(2);
      String minutesText = (seconds / 60).toStringAsFixed(2);

      if (factor != 1) {
        secondsController.text = secondsText;
      }

      if (factor != 60) {
        minutesController.text = minutesText;
      }

      if (factor != 3600) {
        hoursController.text = hoursText;
      }
    } on FormatException { }
  }

  @override
  void dispose() {
      secondsController.dispose();
      minutesController.dispose();
      hoursController.dispose();
      super.dispose();
    }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("Seconds: "),
            TextField(controller: secondsController, onChanged: (String newValue) {
              setTime(newValue, 1);
            }),
            Text("Minutes: "),
            TextField(controller: minutesController, onChanged: (String newValue) {
              setTime(newValue, 60);
            }),
            Text("Hours: "),
            TextField(controller: hoursController, onChanged: (String newValue) {
              setTime(newValue, 3600);
            }),
          ],
        ),
      ),
    );
  }
}

נשים לב לפרטים:

  1. קוד flutter מורכב מ Widgets. בקוד הדוגמה יש ווידג'ט ראשי בשם MyApp ובתוכו ווידג'ט משני שנקרא MyHomePage.

  2. ווידג'ט יכול להיות Stateless או Stateful - כשהשאלה היא האם הוא משתנה במהלך ריצת התוכנית. פלאטר ידאג לעדכן את הממשק של Stateful Widget כל פעם שהסטייט משתנה.

  3. הסטייט נשמר במחלקה נפרדת והוא איפה שכל הלוגיקה יושבת. במערכת אמיתית בטח הייתי שומר אותו בקובץ אחר, ובדוגמה שלנו הקלאס נקרא _MyHomePageState.

  4. בגלל שדארט היא שפה מונחית עצמים וכל ה API של פלאטר מבוסס על מחלקות, גם הסטייט הוא מחלקה בעצמו. קצת מבלבל של Stateless Widget יש פונקציית build שבונה אותו, אבל ל Stateful Widget יש רק פונקציית createStaet ולסטייט שלו יש פונקציית build.

  5. נדלג על המתודה setTime ועל dispose כדי להגיד כמה מילים על build: הפונקציה מחזירה את הייצוג של ה Widget ותיקרא כל פעם שהסטייט ישתנה. שימו לב לסידור של הילדים בתוך אוביקט Column מה שאומר שכולם יופיעו אחד מתחת לשני. חוץ מ Column יש עוד כמה דרכים לארגן ווידג'טים על המסך וזה אחד הרעיונות הנחמדים של פלאטר - כי בעצם כל העיצוב מובנה בתוך הקוד.

  6. הפונקציה האנונימית שעוברת בתור ערך של הפרמטר onChanged ב TextField תיקרא כל פעם שמשתמש מעדכן את השדה. כל שדה משתמש ב setTime עם ערך בסיס אחר וזה מה שגורם לעדכון שני השדות האחרים בכל עדכון. המשתנה השני controller הוא שמאפשר לי לשנות את הערך בתיבה בצורה תכנותית.

  7. לא מקובל לשמור Reference לאלמנטים עצמם שיצרנו בתוך ה Build. הדרך המרכזית לתקשר עם ה Widgets שאנחנו יוצרים היא להשתמש במשתני סטייט כשיוצרים אותם (כי אז build ייקרא שוב כשמשתנים אלו יתעדכנו). במקרה של שדות טקסט זה הקונטרולר.

  8. לסיום הפונקציה setTime שעושה את כל העבודה מספרת יותר על Dart מאשר על פלאטר: הקוד כולו בתוך try/catch כי double.parse עלול לזרוק Exception אם הערך לא נראה כמו מספר; לכל משתנה יש טיפוס; בחלק מהמקומות בקוד שלי אפשר היה לוותר על המילה String לפני משתנה כי dart יכול להבין את זה לבד.

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

סך הכל ולמרות שהקוד עובד אני חייב להודות שעבור אפליקציית הדוגמה שבחרתי פלאטר ו dart נראו לי מסורבלים מדי. ההמרות בין מספרים למחרוזות והטיפול ב Exceptions הפכו את הקוד ליותר ארוך ממה שאני רגיל לראות בעולם ה Web. מצד שני האפליקציה רצה בתור Desktop App על כל מערכות ההפעלה ובתור Web App בדפדפן.

קוד האפליקציה המלא זמין בגיטהאב בקישור: https://github.com/ynonp/hello-flutter-world.