תחי ישראל - אין לנו ארץ אחרת

תחי ישראל -אין לנו ארץ אחרת

CRUD באפליקציית Flutter - מדריך שני : קריאה ממסד הנתונים

מחבר:
בתאריך:

המדריך השני בסדרה על CRUD ע"ג מסד נתונים Firebase במסגרת אפליקציית Flutter מלמד כיצד לקרוא את רשימת המכוניות ממסד הנתונים וכיצד להציג בתוך אפליקציה.

Read list of items from Firebase database and present it inside a stream in a Flutter app

 

להורדת קוד אפליקציית Flutter לעבודה עם מסד הנתונים של Firebase

 

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

 

המודל המגדיר Car מהו

נוסיף מודל Car שיהווה את התבנית של המידע לו אנו מצפים עבור כל פריט מידע באפליקציה שהוא במקרה שלנו מכונית:

lib/models/car_model.dart

class Car {
 String? key;
 CarData carData;
 
 Car({this.key, required this.carData});
}
  • הקלאס מאורגן על פי מבנה המידע במסד הנתונים כאשר לכל רשומה יש key ו-value. ה-key הוא מחרוזת אקראית שמקצה Firebase לכל רשומה חדשה שמתווספת בו נשתמש לזיהוי הרשומה. ה-value מכיל את שדות המידע (מחיר, שם וכיו"ב) שפה אנו מכנים "carData".

נארגן את ה-carData עצמו בקלאס שיכלול את כל השדות שאנו מצפים שיהיו במכונית:

lib/models/car_model.dart

class CarData {
 String? name;
 double? price;
 bool? isFavorite;
 DateTime? date;
 
 CarData({this.name, this.price, this.isFavorite, this.date});
}
  • הקונסטרקטור ישמש ליצירת אובייקט מידע CarData חדש.

ב-dart לאותו קלאס יכולים להיות מספר קונסטרקטורים. נוסיף קונסטרקטור שיהפוך מידע שמקורו ב- JSON, כמו זה שמתקבל ממסד נתונים Firebase, ל-CarData:

lib/models/car_model.dart

class CarData {
 String? name;
 double? price;
 bool? isFavorite;
 DateTime? date;
 
 CarData({this.name, this.price, this.isFavorite, this.date});
 
 CarData.fromJson(Map<dynamic, dynamic> json) {
   name = json["name"] ?? 'N/A';
   price = json["price"].toDouble() ?? 0.0;
   isFavorite = json["isFavorite"] ?? false;
   date = (json['date'] != null)
       ? DateTime.fromMicrosecondsSinceEpoch(json["date"] as int)
       : DateTime.now();
 }
}
  • כל אחד מהשדות מקבל ערך במידה וקיים, או ערך ברירת מחדל בהתאם לסוג המשתנה.

 

לסיכום, כך נראה עכשיו קוד המודל:

lib/models/car_model.dart

class CarData {
  String? name;
  double? price;
  bool? isFavorite;
  DateTime? date;

  CarData({this.name, this.price, this.isFavorite, this.date});

  CarData.fromJson(Map json) {
    name = json["name"] ?? 'N/A';
    price = json["price"].toDouble() ?? 0.0;
    isFavorite = json["isFavorite"] ?? false;
    date = (json['date'] != null)
        ? DateTime.fromMicrosecondsSinceEpoch(json["date"] as int)
        : DateTime.now();
  }
}

class Car {
  String? key;
  CarData carData;

  Car({this.key, required this.carData});
}

 

שליפת מידע עבור פריט ספציפי ממסד הנתונים ותזכורת לגבי Provider

נוסיף למודל CarsProvider מתודה א-סינכרונית שתקבל פרמטר key של רשומה במסד הנתונים ותחזיר את המידע ברשומה:

Future<CarData> getByKey(String key) async {
   try {
     final snapshot = await _db.child(carsPath).child(key).get();
     if (snapshot.exists && snapshot.value != null) {
       return CarData.fromJson(snapshot.value as Map<dynamic, dynamic>);
     }
     throw 'Problem retrieving data';
   } catch (err) {
     rethrow;
   }
}
  • במידה והמידע קיים, המתודה מחזירה את הרשומה בצורת אובייקט מסוג CarData אחרת היא זורקת שגיאה.

רגע לפני שנשתמש במתודה לשליפת מידע עבור פריטים ממסד הנתונים חשוב לזכור שהקשר בין הקלאס שעובד מול מסד הנתונים לבין הווידג'טים שמציגים את המידע למשתמשים נעשה באמצעות דפוס provider. המידע נקלט ממקור נתונים (לדוגמה: מסד נתונים) על ידי מודל היורש את ChangeNotifier. כשהמידע במודל (state) משתנה (בגלל שינוי המידע במקור הנתונים) קריאה ל-NotifyListenters גורמת למידע לזרום ל-Consumer שמעדכנים את התצוגה בווידג'טים (הקליקו כאן כדי לרענן את זכרונכם בנושא חבילת provider).

בווידג'ט שמציג את הטופס נקלוט את ערך ה-key, ונשתמש ב-Provider.of כדי לתקשר עם המתודה getByKey לצורך שליפת המידע עבור הפריט הרצוי ממסד הנתונים. במידה והמידע שחוזר תקין נאכלס בו את שדות הטופס. במקרה של שגיאה נקפיץ הודעת שגיאה:

lib/screens/home_screen.dart

Future<void> _carDialog(String key) async {
   var cachedCtx = context;
 
   // clear the form fields
   _editNameController.text = '';
   _editPriceController.text = '';
 
   // if key not empty
   if (key != '') {
     // get the data about a record from the provider
     Provider.of<CarsProvider>(context, listen: false)
         .getByKey(key)
         .then((CarData data) {
       // populate the form fields with the retrieved data
       _editNameController.text = data.name.toString();
       _editPriceController.text = data.price.toString();
     }).catchError((errMsg) {
       // show error message in case of an error
       setState(() {
         _errorMsg = errMsg;
       });
     });
   }
 
   // the rest of the dialog including the form
    // showDialog() et al
}

 

שליפת רשימת פריטי המידע ממסד הנתונים

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

נוסיף למודל CarsProvider רשימה של מכוניות ומשתנה שיאמר לנו האם המידע בטעינה. שניהם משתנים פרטיים:

lib/providers/cars_provider.dart

class CarsProvider extends ChangeNotifier {
  final DatabaseReference _db = FirebaseDatabase.instance.ref();
  static const carsPath = 'Cars';
 
  List<Car> _cars = [];
  late bool _isLoading = false;
 
  // the rest of the code...
}
  • משתנה db מחזיק את המצביע (רפרנס) למסד הנתונים.
  • קבוע carsPath מחזיק את שם הקולקציה במסד הנתונים.
  • המשתנה cars מחזיק את רשימת המכוניות שהיא כרגע ריקה.
  • המשתנה isLoading הופך ל-true בזמן הטעינה של הנתונים וביתר הזמן ערכו false.

נוסיף 2 פונקציות getter בשביל שאפשר יהיה לעבוד מבחוץ עם המשתנים הפרטיים שזה עתה יצרנו:

lib/providers/cars_provider.dart

class CarsProvider extends ChangeNotifier {
  final DatabaseReference _db = FirebaseDatabase.instance.ref();
  static const carsPath = 'Cars';
  late bool _isLoading = false;
 
  List<Car> _cars = [];
 
  List<Car> get cars => List.unmodifiable(_cars);
;
 
  bool get isLoading => _isLoading;
  // the rest of the code..
}
  • ה-getter מחזיר עותק בלתי ניתן לשינוי של המשתנה cars כדי למנוע מצב שבו ניתן לשנות את הרשימה מבחוץ. זה חשוב מפני שרשימות של dart הם reference type מה שאומר שניתן לשנות אותם מכל מקום באפליקציה, ואת זה אנחנו רוצים למנוע כדי לשמור על אמינות המידע ברשימה.

נוסיף מתודה שתאזין לשינויים במידע אודות המכוניות במסד הנתונים:

lib/providers/cars_provider.dart

void _listenToCars() {
  _db
       .child(carsPath)
       .orderByChild("date")
       .limitToLast(10)
       .onValue
       .listen((event) { 
         // 1. List of records from db to map
         // 2. Each database record to map
       });
}
  • המאזין נרשם לזרם המידע Stream ועל כן מתעדכן ומגיב על כל שינוי באותו חלק של מסד הנתונים אליו הוא רשום (במקרה זה, מאזין למידע בטבלה Cars במסד הנתונים של Firebase).
  • המתודה שולפת את 10 הרשומות האחרונות.

נעדכן את רשימת המכוניות במידע המתקבל ממסד הנתונים ב-2 שלבים:

// 1. Create a map from the list of db records
final carsList = Map<String, dynamic>.from(event.snapshot.value as dynamic);
 
// 2. Create a Car object from each db record
_cars = carsList.entries.map((entry) {
  return Car(key: entry.key, carData: CarData.fromJson(entry.value));
}).toList();
  1. הפכנו את רשימת הפריטים ששלפנו ממסד הנתונים ל-Map (של מכוניות, במקרה זה).
  2. הפכנו כל אחד מפריטי הרשימה במסד הנתונים לאובייקט Car.

את פריטי המידע נאסוף לרשימה באמצעות toList, ונציב בתור ערך המשתנה הפרטי cars.

אחרי שסיימנו לאסוף את המידע לרשימה אנחנו צריכים להודיע לכל הווידג'טים המאזינים ל-provider שהם צריכים לעבור בנייה מחדש באמצעות הפעלת הפונקציה:

notifyListeners();

נגדיר את ההרשמה לזרם המידע שמגיע ממסד הנתונים כ-handle מסוג Stream Subscription שניתן לו את השם carsStream:

class CarsProvider extends ChangeNotifier {
 final DatabaseReference _db = FirebaseDatabase.instance.ref();
 static const carsPath = 'Cars';
 
 List<Car> _cars = [];
 late bool _isLoading = false;
 
 List<Car> get cars => _cars;
 
 bool get isLoading => _isLoading;
 
 late StreamSubscription<dynamic> _carsStream;
 
 // the rest of the code…
}

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

lib/providers/cars_provider.dart

@override
void dispose() {
   _carsStream.cancel();
   super.dispose();
} 

בין ההרשמה לזרם המידע בזמן המיידי שאחרי הקמת האובייקט מהקלאס לבין חיסול ההרשמה נציב ל-StreamSubscription את הקוד המאזין למסד הנתונים:

lib/providers/cars_provider.dart

void _listenToCars() {
   _isLoading = true;
 
   _carsStream = _db
       .child(carsPath)
       .orderByChild("date")
       .limitToLast(10)
       .onValue
       .listen((event) {
     if (event.snapshot.value == null) {
       _cars = [];
     } else {
       final carsList =
           Map<String, dynamic>.from(event.snapshot.value as dynamic);
       _cars = carsList.entries.map((entry) {
         return Car(key: entry.key, carData: CarData.fromJson(entry.value));
       }).toList();
       _cars.sort((a, b) => b.carData.date!.compareTo(a.carData.date!));
     }
     _isLoading = false;
     notifyListeners();
   }, onError: (error) {
     //print(error.toString()); > notify the user
     _cars = [];
     _isLoading = false;
     notifyListeners();
   }, cancelOnError: false,
   onDone: (() {
     // what to do when the listener stops
   }));
 }
  • מיד כשהפונקציה מתחילה לרוץ נשנה את ערך המשתנה isLoading ל-true. בטמפלייט נשתמש במידע כדי להראות אנימציה של טעינה בזמן שמחכים לקבלת המידע.
  • הידית של ה-StreamSubscription מקבלת לתוכה את זרם המידע המגיע ממסד הנתונים.
  • במקרה שהמידע המוחזר ממסד הנתונים הוא null נאפס את מערך הרשימה cars.
  • במידה ומתקבלים פריטי מידע נהפוך אותם לרשימה של מכוניות.
  • את רשימת המכוניות סידרנו בסדר יורד של תאריכים באמצעות:
_cars.sort((a, b) => b.carData.date!.compareTo(a.carData.date!));

והצבנו למשתנה cars.

  • אחרי הבאת מלוא רשימת הפריטים ממסד הנתונים נעדכן את המאזינים ל-provider על השינוי בתכולת המידע באמצעות notifyListeners.
  • נקטנו משנה זהירות, כמתבקש בתכנות, והוספנו handle לטיפול בשגיאות onError.

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

נקרא למתודה המאזינה מתוך קונסטרוקטור הקלאס:

lib/providers/cars_provider.dart

CarsProvider() {
   _listenToCars();
 }

 

לסיכום, כך נראה עכשיו הקוד המלא של הקלאס CarsProvider:

lib/screens/cars_provider.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';

import '../models/car_model.dart';

class CarsProvider extends ChangeNotifier {
  final DatabaseReference _db = FirebaseDatabase.instance.ref();
  static const carsPath = 'Cars';

  List _cars = [];
  late bool _isLoading = false;

  List get cars => [..._cars];

  bool get isLoading => _isLoading;

  late StreamSubscription _carsStream;

  CarsProvider() {
    _listenToCars();
  }

  void _listenToCars() {
    _isLoading = true;

    _carsStream =
        _db.child(carsPath).orderByChild("date").limitToLast(10).onValue.listen(
            (event) {
              if (event.snapshot.value == null) {
                _cars = [];
              } else {
                // 1. Create a map from the list of db records
                final carsList =
                    Map.from(event.snapshot.value as dynamic);
                // 2. Create a Car object from each db record
                // 3. Make a list of Car objects
                _cars = carsList.entries.map((entry) {
                  return Car(
                      key: entry.key, carData: CarData.fromJson(entry.value));
                }).toList();
                // sort by date
                _cars
                    .sort((a, b) => b.carData.date!.compareTo(a.carData.date!));
              }
              _isLoading = false;
              notifyListeners();
            },
            onError: (error) {
              //print(error.toString()); > notify the user
              _cars = [];
              _isLoading = false;
              notifyListeners();
            },
            cancelOnError: false,
            onDone: (() {
              // what to do when the listener stops
            }));
  }

  Future add(String name, double price) async {
    try {
      final Map data = {
        "name": name.toString(),
        "price": price,
        "isFavorite": false,
        "date": DateTime.now().millisecondsSinceEpoch,
      };
      await _db.child(carsPath).push().set(data);
      return true;
    } catch (err) {
      return false;
    }
  }

  Future getByKey(String key) async {
    try {
      final snapshot = await _db.child(carsPath).child(key).get();
      if (snapshot.exists && snapshot.value != null) {
        return CarData.fromJson(snapshot.value as Map);
      }
      throw 'Problem retrieving data';
    } catch (err) {
      rethrow;
    }
  }

  @override
  void dispose() {
    _carsStream.cancel();
    super.dispose();
  }
}

 

הצגת הרשימה המעודכנת של המכוניות

את רשימת המכוניות נציג בתוך Consumer שמקבל נוטיפיקציה בכל פעם שה-state (מידע) של ה-provider אליו הוא שייך משתנה. Consumer מצויד במתודה מסוג builder שבונה מחדש את הווידג'ט השייך אליו בהתאם למצב המידע.

lib/screens/home_screen.dart

@override
Widget build(BuildContext context) {
   final appBar = AppBar(
     title: const Text("My garage"),
   );
   return Scaffold(
     appBar: appBar,
     body: Padding(
       padding: const EdgeInsets.all(16.0),
       child: SingleChildScrollView(
         child: Column(
           children: [
             Consumer<CarsProvider>(
               builder: ((context, model, child) {
                    // the widget to rebuild whenever notifyListeners fires
                }),
             )
           ],
         ),
       ),
     ),
    );
 }
  • ה-Consumer חייב לקבל את סוג המודל שהוא מאזין אליו. במקרה זה, CarsProvider.
  • הפרמטר השני שמועבר ל-Consumer הוא האובייקט שאיתו אנחנו רוצים לעבוד (במקרה שלנו, המודל CarsProvider) שבקוד לעיל קראתי לו model.

לצורך הצגת הרשימה נשתמש בווידג'ט ListView בתוכו כל פריט יהיה ב-Card נפרד:

// the widget to rebuild whenever notifyListeners fires
ListView(
  children: [
    if (model.cars.isNotEmpty)
     ...model.cars.map((car) => Card(
        // card widget content
      ),
    ),
  ],
);
  • במידה והרשימה אינה ריקה, cars.isNotEmpty נמפה כל אחד מפריטי הרשימה לווידג'ט Card משלו.
  • שלוש הנקודות ... הם spread operator מאפשרים להכניס רשימה לתוך רשימה קיימת. במקרה שלנו, מאפשרים לחבר את רשימת המכוניות לתוך רשימת ה-children של הווידג'ט.

בתוך כל Card נשים ווידג'ט נוסף מסוג ListTile שיציג את שם המכונית ואת מחירה:

Card(
  // card widget content
  child: ListTile(
  leading: const Icon(Icons.directions_car),
  title: Text(car.carData.name!),
  subtitle: Text('${car.carData.price} $')
  ),
),

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

Card(
  child: ListTile(
  leading: const Icon(Icons.directions_car),
  title: Text(car.carData.name!),
  subtitle: Text('${car.carData.price} $'),
  trailing: SizedBox(
    width: 120,
    child: Row(children: []),
   ),
  ),
),

נוסיף בתוך ה-Row את שלושת כפתורי הפעולה כל אחד בתוך ווידג'ט מסוג Expanded מה שיגרום לכל אחד מהכפתורים לתפוס שליש מרוחב ה-Row:

Row(children: [
  Expanded(
    child: IconButton(
    icon: car.carData.isFavorite == false
      ? const Icon(Icons.favorite_border)
      : const Icon(Icons.favorite),
     color: Colors.red,
     onPressed: () {   },   
     ),                                                                            
   ),
   Expanded(
     child: IconButton(
     icon: const Icon(Icons.edit_note),
     color: Colors.grey,
     onPressed: () { },
    ),
   ),
   Expanded(
     child: IconButton(
       icon: const Icon(Icons.delete_forever_sharp),
       color: Colors.grey,
       onPressed: () {},
     ),
   ),
]),

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

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

ListView(
  children: [
    if (model.isLoading) ...[
      const Center(child: CircularProgressIndicator()),
     ] else if (model.cars.isEmpty) ...[
       const Center(child: Text("No cars found")),
     ] else if (model.cars.isNotEmpty)
       ...model.cars.map((car) => Card(
        // card widget content
      ),
    ),
  ],
);

בהצגת הרשימה אנו מתחשבים בשלושה מצבים:

  • במקרה שהנתונים נטענים נציג אנימציה
  • במקרה שהרשימה ריקה נציג הודעה למשתמש
  • רק במקרה בו קיימים פריטים ברשימה נציג את הרשימה

 

הצגת המחיר כולל מפרידי אלפים

שיפור נוסף הוא הצגת המחיר כולל מפרידי אלפים. במקום 1000000 נציג 1,000,000. נייבא את הספרייה intl שהיא מאוד חשובה וכדאי להכיר, במיוחד בגלל שאנחנו עובדים בעברית בשוק הישראלי. לפרטי חבילת intl והוראות התקנה באתר pub.dev

אחרי ההתקנה נייבא את החבילה לסקריפט :

lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
// the rest of the dependencies

ניצור משתנה פורמטר המתבסס על המתודה NumberFormat של חבילת intl:

var priceFormatter = NumberFormat('###,###,###');

נשתמש בפורמטר לצורך הצגת המחיר:

Text('${priceFormatter.format(car.carData.price)} $')

הקוד המלא ששימש להצגת רשימת המכוניות בתוך המתודה build:

lib/screens/home_screen.dart

@override
 Widget build(BuildContext context) {
   final appBar = AppBar(
     title: const Text("My garage"),
   );
   return Scaffold(
     appBar: appBar,
     body: Padding(
       padding: const EdgeInsets.all(16.0),
       child: SingleChildScrollView(
         child: Column(
           children: [
             SizedBox(
               height: 700,
               child: Consumer<CarsProvider>(
                 builder: ((context, model, child) {
                   // the widget to rebuild whenever notifyListeners fires
                   return ListView(
                     children: [
                       if (model.isLoading) ...[
                         const Center(child: CircularProgressIndicator()),
                       ] else if (model.cars.isEmpty) ...[
                         const Center(child: Text("No cars found")),
                       ] else if (model.cars.isNotEmpty)
                         ...model.cars.map(
                           (car) => Card(
                             // card widget content
                             child: ListTile(
                               leading: const Icon(Icons.directions_car),
                               title: Text(car.carData.name!),
                               subtitle: Text('${car.carData.price} $'),
                               trailing: SizedBox(
                                 width: 120,
                                 child: Row(children: [
                                   Expanded(
                                     child: IconButton(
                                       icon: car.carData.isFavorite == false
                                           ? const Icon(Icons.favorite_border)
                                           : const Icon(Icons.favorite),
                                       color: Colors.red,
                                       onPressed: () {},
                                     ),
                                   ),
                                   Expanded(
                                     child: IconButton(
                                       icon: const Icon(Icons.edit_note),
                                       color: Colors.grey,
                                       onPressed: () {},
                                     ),
                                   ),
                                   Expanded(
                                     child: IconButton(
                                       icon: const Icon(
                                           Icons.delete_forever_sharp),
                                       color: Colors.grey,
                                       onPressed: () {},
                                     ),
                                   ),
                                 ]),
                               ),
                             ),
                           ),
                         ),
                     ],
                   );
                 }),
               ),
             )
           ],
         ),
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         _carDialog("");
       },
       backgroundColor: Colors.blue,
       child: const Icon(Icons.add),
     ),
   );
 }

ארוך מדי; קשה לקרוא

 

מיצוי ווידג'טים והפיכת ווידג'ט ארוך ולא קריא לקצר וקל לעיכול

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

כדי למצות ווידג'ט ב-VScode אני עומד על שמו עם העכבר ומקליק על הנורה שמופיעה ואז בוחר באפשרות:Extract Widget

שימוש באפשרות Extract Widget של VScode כדי למצות ווידג'ט מתוך קוד Flutter קיים

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

lib/screens/home_screen.dart

class CarCardWidget extends StatelessWidget {
 const CarCardWidget({
   super.key,
 });
 
 @override
 Widget build(BuildContext context) {
   return Card(
     // card widget content
     child: ListTile(
       leading: const Icon(Icons.directions_car),
       title: Text(car.carData.name!),
       subtitle: Text('${car.carData.price} $'),
       trailing: SizedBox(
         width: 120,
         child: Row(children: [
           Expanded(
             child: IconButton(
               icon: car.carData.isFavorite == false
                   ? const Icon(Icons.favorite_border)
                   : const Icon(Icons.favorite),
               color: Colors.red,
               onPressed: () {},
             ),
           ),
           Expanded(
             child: IconButton(
               icon: const Icon(Icons.edit_note),
               color: Colors.grey,
               onPressed: () {},
             ),
           ),
           Expanded(
             child: IconButton(
               icon: const Icon(
                   Icons.delete_forever_sharp),
               color: Colors.grey,
               onPressed: () {},
             ),
           ),
         ]),
       ),
     ),
   );
 }
}

במקום שבו היה קוד הווידג'ט שזה עתה מיצינו תהיה עכשיו קריאה לווידג'ט CarCardWidget:

lib/screens/home_screen.dart

return ListView(
  children: [
    if (model.isLoading) ...[
      const Center(child: CircularProgressIndicator()),
    ] else if (model.cars.isEmpty) ...[
       const Center(child: Text("No cars found")),
     ] else if (model.cars.isNotEmpty)
       ...model.cars.map(
          (car) => CarCardWidget(),
        ),
      ],
);
  • אלגנטי.

נעביר לווידג'ט CarCardWidget את הערך של פריט רשימה מסוג Car בקונסטרקטור. גם מצד הקריאה לווידג'ט:

lib/screens/home_screen.dart

...model.cars.map(
          (car) => CarCardWidget(car),

וגם בקונסטרקטור של הווידג'ט:

lib/screens/home_screen.dart

class CarCardWidget extends StatelessWidget {
  final Car car;
  const CarCardWidget(
   this.car, {
   super.key,
  });
  // the rest of the widget
}

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

נוסיף תיקיית widgets ובתוכה קובץ dart ששמו car_card_widget.dart:

lib/widgets/car_card_widget.dart

import 'package:flutter/material.dart';
 
import '../models/car_model.dart';
 
class CarCardWidget extends StatelessWidget {
 final Car car;
 const CarCardWidget(
   this.car, {
   super.key,
 });
 
 @override
 Widget build(BuildContext context) {
   return Card(
     // card widget content
     child: ListTile(
       leading: const Icon(Icons.directions_car),
       title: Text(car.carData.name!),
       subtitle: Text('${car.carData.price} $'),
       trailing: SizedBox(
         width: 120,
         child: Row(children: [
           Expanded(
             child: IconButton(
               icon: car.carData.isFavorite == false
                   ? const Icon(Icons.favorite_border)
                   : const Icon(Icons.favorite),
               color: Colors.red,
               onPressed: () {},
             ),
           ),
           Expanded(
             child: IconButton(
               icon: const Icon(Icons.edit_note),
               color: Colors.grey,
               onPressed: () {},
             ),
           ),
           Expanded(
             child: IconButton(
               icon: const Icon(Icons.delete_forever_sharp),
               color: Colors.grey,
               onPressed: () {},
             ),
           ),
         ]),
       ),
     ),
   );
 }
}

נוסיף את קובץ הווידג'ט כתלות ל-home_screen:

lib/screens/home_screen.dart

import '../widgets/car_card_widget.dart';

 

לסיכום, כך נראה הווידג'ט:

lib/widgets/car_card_widget.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/car_model.dart';

class CarCardWidget extends StatelessWidget {
  final Car car;
  const CarCardWidget(
    this.car, {
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      // card widget content
      child: ListTile(
        leading: const Icon(Icons.directions_car),
        title: Text(car.carData.name!),
        subtitle: 
            Text('${NumberFormat('###,###,###').format(car.carData.price)} $'),
        trailing: SizedBox(
          width: 120,
          child: Row(children: [
            Expanded(
              child: IconButton(
                icon: car.carData.isFavorite == false
                    ? const Icon(Icons.favorite_border)
                    : const Icon(Icons.favorite),
                color: Colors.red,
                onPressed: () {},
              ),
            ),
            Expanded(
              child: IconButton(
                icon: const Icon(Icons.edit_note),
                color: Colors.grey,
                onPressed: () {},
              ),
            ),
            Expanded(
              child: IconButton(
                icon: const Icon(Icons.delete_forever_sharp),
                color: Colors.grey,
                onPressed: () {},
              ),
            ),
          ]),
        ),
      ),
    );
  }
}

 

לסיכום, כך נראה הטמפלייט home_screen:

lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';

import '../models/car_model.dart';
import '../providers/cars_provider.dart';
import '../widgets/car_card_widget.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State createState() => _HomeScreenState();
}

class _HomeScreenState extends State {
  final _formKey = GlobalKey<FormState>();

  final TextEditingController _editNameController = TextEditingController();
  final TextEditingController _editPriceController = TextEditingController();

  var _errorMsg = '';

  var priceFormatter = NumberFormat('###,###,###');

  @override
  Widget build(BuildContext context) {
    final appBar = AppBar(
      title: const Text("My garage"),
    );
    return Scaffold(
      appBar: appBar,
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: SingleChildScrollView(
          child: Column(
            children: [
              SizedBox(
                height: 700,
                child: Consumer(
                  builder: ((context, model, child) {
                    // the widget to rebuild whenever notifyListeners fires
                    return ListView(
                      children: [
                        if (model.isLoading) ...[
                          const Center(child: CircularProgressIndicator()),
                        ] else if (model.cars.isEmpty) ...[
                          const Center(child: Text("No cars found")),
                        ] else if (model.cars.isNotEmpty)
                          ...model.cars.map(
                            (car) => CarCardWidget(car),
                          ),
                      ],
                    );
                  }),
                ),
              )
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _carDialog("");
        },
        backgroundColor: Colors.blue,
        child: const Icon(Icons.add),
      ),
    );
  }

  Future _carDialog(String key) async {
    // We'll use this to pop the route
    var cachedCtx = context;

    _editNameController.text = '';
    _editPriceController.text = '';

    // if key not empty
    if (key != '') {
      // get the data about a record from the provider
      Provider.of(context, listen: false)
          .getByKey(key)
          .then((CarData data) {
        // populate the form fields with the retrieved data
        _editNameController.text = data.name.toString();
        _editPriceController.text = data.price.toString();
      }).catchError((errMsg) {
        // show error message in case of an error
        setState(() {
          _errorMsg = errMsg;
        });
      });
    }

    showDialog(
        context: context,
        builder: (context) {
          return Dialog(
            child: Form(
              key: _formKey,
              child: Container(
                padding: const EdgeInsets.all(10),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    if (_errorMsg != '')
                      Text(
                        _errorMsg,
                        style: const TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.red,
                        ),
                      ),
                    TextFormField(
                      controller: _editNameController,
                      decoration: const InputDecoration(helperText: "Name"),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter name';
                        }
                        return null;
                      },
                    ),
                    TextFormField(
                      controller: _editPriceController,
                      decoration: const InputDecoration(helperText: "Price"),
                      keyboardType:
                          const TextInputType.numberWithOptions(decimal: true),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter price';
                        }
                        return null;
                      },
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    ElevatedButton(
                      onPressed: () {
                        if (_formKey.currentState!.validate()) {
                          final formName =
                              _editNameController.text.trim().toString();
                          final formPrice =
                              double.parse(_editPriceController.text.trim());

                          Provider.of(context, listen: false)
                              .add(formName, formPrice)
                              .then((success) {
                            if (success) {
                              if (!mounted) return;
                              Navigator.of(cachedCtx).pop();
                            } else {
                              _errorMsg = 'Problem adding a new car';
                            }
                          });
                        }
                      },
                      child: const Text("Save"),
                    )
                  ],
                ),
              ),
            ),
          );
        });
  }
}

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

 

גם זה עשוי לעניין אותך

שימוש ב-provider באפליקציית Flutter

הקמת אפליקציית Flutter פשוטה על גבי מסד נתונים Firebase

CRUD באפליקציית Flutter - מדריך ראשון : כתיבה למסד נתונים

 

לכל המדריכים בסדרת ה- Flutter

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

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

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

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

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

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

הוסף תגובה חדשה

 

 

ענה על השאלה הפשוטה הבאה כתנאי להוספת תגובה:

איך קוראים בעברית לצ`ופצ`יק של הקומקום?