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

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

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

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

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

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

using provider package in flutter app

 

להורדת הקוד אותו נפתח במדריך

 

מכיוון שאנחנו עובדים עם קלאס של מכוניות נגדיר מהי מכונית באמצעות קלאס שישב בתיקיית models (התיקייה של הקלאסים הרגילים באפליקציה):

lib/models/car.dart

class Car {
 final int id;
 final String name;
 bool isFavorite;
 
 // Constructor to initialize the object
 Car({
   required this.id,
   required this.name,
   this.isFavorite = false,
 });
}
  • לכל מכונית חייב להיות מזהה ייחודי, id, ושם. את התכונה "isFavorite" הגדרתי false כדי לאפשר למשתמש לבחור האם המכונית חביבה עליו.

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

ישנם שני סוגי state באפליקציות Flutter:

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

אין דרך ישירה להעביר מידע app state בין ווידג'טים. הדרך הפשוטה ביותר לנהל state באפליקציית Flutter הוא באמצעות חבילת Provider של Dart.

אנחנו עובדים עם 4 סוגים של קומפננטות כשאנחנו משתמשים בחבילת Provider:

  • קלאס היורש את ChangeNotifier שמחזיק state ונאזין לו כדי לקבל עדכונים.
  • ChangeNotifierProvider - ווידג'ט העוטף את הווידג'טים שצריכים להאזין ל ChangeNotifier.
  • בתוך הווידג'טים נשתמש ב-Consumer שמאזינים לשינויים ב-state ובהתאם בונים מחדש את התצוגה.

Provider.of - מאפשר לווידג'ט לגשת למידע בלי לבנות מחדש את התצוגה.

תרשים הזרימה הבא מסכם את הדרך שבה דפוס ה-provider מיושם ב-Flutter:

flow of the way the provider pattern works in Flutter

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

 

יבוא ספריית Provider לתוך פרויקט Flutter

בשביל לייבא את ספריית Provider, נכנס לקישור ב-pub.dev (האתר של ספריות ה-Flutter) https://pub.dev/packages/provider ומלשונית installing נעתיק את הקוד להתקנת החבילה, ונריץ בכלי שורת הפקודות טרמינל:

$ flutter pub add provider

 

קלאס מנהל מידע ChangeNotifier

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

lib/providers/cars.dart

import 'package:flutter/material.dart';
 
class Cars extends ChangeNotifier {
}
  • הקלאס יורש את ChangeNotifier אשר מגיע מהספרייה flutter/material . ChangeNotifier מספק נוטיפיקציות על שינוי המידע למאזינים שרשומים אליו.
  • הקלאס אינו מודל רגיל כמו Car, שרק מחזיק מידע. אלא provider שמספק ומנהל מידע. לכן מקובל למקם אותו בתיקייה נפרדת מיתר המודלים, בתיקיית providers.

הקלאס Cars מנהל רשימת פריטי מידע. במקרה שלנו, פריטים מסוג Car:

lib/providers/cars.dart

import 'package:flutter/material.dart';
import '../models/car.dart';
 
class Cars extends ChangeNotifier {
 // Private so nothing can change it from outside
 final List<Car> _items = [
   Car(id: 1, name: "Tesla", isFavorite: false),
   Car(id: 2, name: "BMW", isFavorite: false),
   Car(id: 3, name: "Mercedes", isFavorite: false)
 ];
}
  • הרשימה מוגדרת פרטית private (ע"י הקו התחתון לפני שם המשתנה) כדי שקוד מבחוץ לא יוכל לשנות אותה.

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

lib/providers/cars.dart

// getter
 List<Car> get items => List.unmodifiable(_items);
  • השימוש ב-List.unmodifiable יוצר עותק של הרשימה שאי אפשר לשנות אותו immutable, על מנת שהקוד אשר משתמש ב-getter לא יגרום לשינוי הרשימה בטעות.

נוסיף מתודת getter שתחזיר פריט רשימה על פי id:

lib/providers/cars.dart

Car findById(int id) {
   return _items.firstWhere((item) => item.id == id);
 }
  • המתודה firstWhere מאפשרת לבחור את הפריט הראשון שערך השדה שלו מתאים לפרמטר החיפוש.

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

נכתוב מתודה add אשר מוסיפה פריט לרשימה ואח"כ מעדכנת את הווידג'טים המאזינים באמצעות notifyListeners:

lib/providers/cars.dart

void add(String name) {
   final newId = _items.length + 1;
   final newItem = Car(id: newId, name: name, isFavorite: false);
   _items.add(newItem);
   // Call the listening widgets so they rebuild
   notifyListeners();
 }
  • המתודה add מקבלת את שם המכונית כארגומנט, יוצרת אובייקט Car, ומוסיפה לרשימה.
  • בשביל ה-id החדש נוסיף לאורך הרשימה 1 (זה לא רעיון טוב בשביל אפליקציות שרצות ב-production אבל בשביל האפליקציה הפשוטה שלנו זה בסדר).
  • אחרי הוספת פריט לרשימה המתודה קוראת ל-notifyListeners כדי לעדכן את הקלאסים המאזינים בכך שהמידע state השתנה, מה שגורם להם לעדכן את התצוגה.

נוסיף מתודה מעדכנת updateFavorite שמקבלת פרמטר id, מעדכנת את מצב המשתנה isFavorite, ובסוף מעדכנת את האפליקציה על כך שה-state השתנה באמצעות notifyListeners:

lib/providers/cars.dart

void updateFavorite(int id) {
    var item = findById(id);
    item.isFavorite = !item.isFavorite;
    notifyListeners();
 }
  • היפוך הערך הבוליאני, מ-false ל-true והפוך, נעשה באמצעות ! סימן קריאה לפני שם המשתנה והצבת הערך המתקבל חזרה בתור ערכו של המשתנה.

המתודה delete מזהה פריט על פי ה-id שלו ואז מסירה מהרשימה:

lib/providers/cars.dart

void delete(int id) {
   _items.removeWhere((item) => item.id == id);
   // Call the listening widgets so they rebuild
   notifyListeners();
 }
  • המתודה removeWhere מזהה פריטים המתאימים לערך החיפוש, ומסירה מהרשימה.
  • אחרי המחיקה מהרשימה המתודה קוראת ל-notifyListeners כדי לעדכן את הקלאסים המאזינים בכך שהמידע state השתנה, מה שגורם להם לעדכן את התצוגה.

 

הטופס להזנת פריטים חדשים

הטופס להזנת פריטים חדשים כולל שדה Name לתוכו מזינים את שם המכונית וכפתור "Add a car" שלחיצה עליו מגישה את הטופס ומעבירה לדף הבית כדי לראות את הרשימה המעודכנת כולל הפריט החדש אותו הוספנו עם הגשת הטופס.

lib/screens/car_edit_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cars.dart';
 
class CarEditScreen extends StatefulWidget {
 const CarEditScreen({super.key});
 
 @override
 State<CarEditScreen> createState() => _CarEditScreenState();
}
 
class _CarEditScreenState extends State<CarEditScreen> {
 final nameController = TextEditingController();
 
 // a global key that uniquely identifies the Form widget
 // and allows validation
 final _formKey = GlobalKey();
 
 void submitForm() {
   var name = nameController.text;
 
   // Validate returns true if the form is valid, or false otherwise.
   if (_formKey.currentState!.validate()) {
     Provider.of<Cars>(context, listen: false).add(name);
     Navigator.pop(context);
   }
 }
 
 @override
 Widget build(BuildContext context) {
   // Build a Form widget using the _formKey created above.
   return Scaffold(
     appBar: AppBar(
       backgroundColor: const Color.fromARGB(255, 33, 150, 243),
       title: const Text("Add a car"),
       centerTitle: true,
     ),
     body: Padding(
       padding: const EdgeInsets.fromLTRB(30.0, 40.0, 30.0, 0.0),
       child: Column(
         children: [
           Form(
             key: _formKey,
             child: Column(
               children: [
                 Padding(
                   padding: const EdgeInsets.all(8.0),
                   child: TextFormField(
                     controller: nameController,
                     validator: (value) {
                       if (value == null || value.isEmpty) {
                         return 'Please enter a name';
                       }
                       return null;
                     },
                     decoration: const InputDecoration(
                       border: OutlineInputBorder(),
                       labelText: 'Name',
                     ),
                   ),
                 ),
                 TextButton(
                   onPressed: submitForm,
                   child: const Text('Add a car'),
                 ),
               ],
             ),
           ),
         ],
       ),
     ),
   );
 }
}

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

lib/screens/car_edit_screen.dart

void submitForm() {
   var name = nameController.text;
 
   // Validate returns true if the form is valid, or false otherwise.
   if (_formKey.currentState!.validate()) {
     Provider.of<Cars>(context, listen: false).add(name);
     Navigator.pop(context);
   }
 }
  • השימוש במתודה Provider.of מאפשר לנו לגשת למתודות של הקלאס Provider מסוג מסוים (Cars במקרה זה) בלי צורך ליצור אובייקט.
  • הגדרת הפרמטר listen של ה-Provider.of כ-false מאפשרת לעבוד עם המתודות של ה-Provider בלי לבנות מחדש את התצוגה.

 

מסך הבית שמציג את הרשימה - פעם ראשונה

מסך הבית של האפליקציה מציג את הרשימה. הוא צריך להיות Stateful בשביל שהמידע בו יתעדכן על פי שינוי המידע state של רשימת המכוניות.

lib/screens/cars_list_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cars.dart';
import './car_edit_screen.dart';
 
class CarsListScreen extends StatefulWidget {
 const CarsListScreen({Key? key}) : super(key: key);
 
 @override
 State<CarsListScreen> createState() => _CarsListScreenState();
}
 
class _CarsListScreenState extends State<CarsListScreen> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('List of cars'),
     ),
     body: Column(
       children: [
           TextButton(
           onPressed: () {
             Navigator.push(
               context,
               MaterialPageRoute(builder: (context) => const CarEditScreen()),
             );
           },
           child: const Text('Add'),
         ),
       ],
     ),
   );
 }
}
  • בתוך ה-scaffold שהוא ווידג'ט בסיסי המתווה את תצוגת הדף, יש לנו AppBar הכולל כותרת, וגוף הדף body המכיל עמודה Column שמאפשרת להציג ווידג'טים אחד מעל לשני.
  • הווידג'ט הראשון ב-Column הוא הקישור לטופס העריכה שלחיצה עליו מנווטת למסך CarEditScreen.
  • בהמשך, נוסיף את הרשימה המתעדכנת של המכוניות.

 

ChangeNotifierProvider ווידג'ט העוטף את הווידג'טים שצריכים להאזין ל ChangeNotifier

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

ב-Flutter, ובכלל בחיים, אנחנו לא מעוניינים לשתף במידע את מי שלא אמור לדעת. לכן, את הווידג'ט ChangeNotifierProvider נמקם רק מעל לווידג'טים שצריכים אותו. במקרה שלנו, כל הווידג'טים צריכים את המידע ולכן נשים אותו בראש ה-widget tree.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/cars.dart';
import 'screens/cars_list_screen.dart';
 
// Register the changeNotifierProvider
void main() => runApp(const MyApp());
 
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
   return MultiProvider(
     providers: [
       ChangeNotifierProvider(
         create: (context) => Cars(),
       ),
     ],
     child: const MaterialApp(
       title: 'Cars',
       home: CarsListScreen(),
     ),
   );
 }
}
  • MultiProvider כולל רשימה של ספקי מידע, providers. במקרה זה, יש לנו קלאס אחד שמספק מידע, Cars.
  • בשדה child נגדיר את הווידג'ט שנרשם להאזנה לספקי המידע, providers. במקרה שלנו, רשמנו את מסך הרשימה CarsListScreen בתור מאזין.
  • ChangeNotifierProvider מספק את המידע רק אם באמת צריך אותו כדי לחסוך במשאבי מחשוב, והוא יודע מתי להסיר את האובייקט של ספק המידע כשלא צריך אותו יותר.

 

מסך הבית שמציג את הרשימה - הצגת רשימת המכוניות המתעדכנת

אחרי שיש לנו גישה ל-state שמספק ה-Provider באמצעות ChangeNotifierProvider אנחנו יכולים להציג את רשימת המכוניות המעודכנת בדף הבית של האפליקציה באמצעות הווידג'ט Consumer שמאזין לעדכוני ה-Provider (הקלאס Cars במקרה שלנו):

lib/screens/cars_list_screen.dart

Consumer<Cars>(
           builder: (context, cars, child) => Stack(
             children: [
               SizedBox(
                 height: 300.0,
                 child: ListView.builder(
                   itemCount: cars.items.length,
                   shrinkWrap: true,
                   itemBuilder: (BuildContext context, int index) {
                     return ListTile(
                       leading: const Icon(Icons.electric_car),
                       title: Text(cars.items[index].name),
                     );
                   },
                 ),
               ),
             ],
           ),
         ),
  • הווידג'ט Consumer כולל את השדה builder שבונה מחדש את התצוגה בכל פעם שהקלאס שהוא מאזין לו משתנה. ה-builder כולל 3 ארגומנטים. השני הוא הכי מעניין מפני שהוא מחזיק את ה-state שאנחנו מעוניינים להציג.
  • את הרשימה אני מציג בתוך ListView.builder שאת גובהו אני מגביל ע"י SizedBox.

 

הוספת כפתורי מחיקה ועדכון לרשימה

נשתמש במתודה delete כדי למחוק פריטים לפי ה-id שלהם:

lib/screens/cars_list_screen.dart

Provider.of<Cars>(context, listen: false)
    .delete(cars.items[index].id);
  • כדי לגשת למתודות של ה-Provider בלי לבנות מחדש את התצוגה אנחנו משתמשים ב-Provider.of המכיל שדה listen שמקבל ערך false מונע את עדכון התצוגה.

נשתמש במתודה updateFavorite כדי לעדכן את הרשימה במכוניות האהובות על המשתמש:

lib/screens/cars_list_screen.dart

Provider.of<Cars>(context, listen: false)
             .updateFavorite(cars.items[index].id);

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

lib/screens/cars_list_screen.dart

Consumer<Cars>(
           builder: (context, cars, child) => Stack(
             children: [
               SizedBox(
                 height: 300.0,
                 child: ListView.builder(
                   itemCount: cars.items.length,
                   shrinkWrap: true,
                   itemBuilder: (BuildContext context, int index) {
                     return ListTile(
                       leading: const Icon(Icons.electric_car),
                       title: Text(cars.items[index].name),
                       trailing: SizedBox(
                         width: 90,
                         child: Row(
                           children: [
                             IconButton(
                               icon: cars.items[index].isFavorite == false
                                   ? const Icon(Icons.favorite_border)
                                   : const Icon(Icons.favorite),
                               color: Colors.red,
                               onPressed: () {
                                 Provider.of<Cars>(context, listen: false)
                                     .updateFavorite(cars.items[index].id);
                               },
                             ),
                             IconButton(
                               icon: const Icon(Icons.delete_forever_sharp),
                               color: Colors.grey,
                               onPressed: () {
                                 Provider.of<Cars>(context, listen: false)
                                     .delete(cars.items[index].id);
                               },
                             ),
                           ],
                         ),
                       ),
                     );
                   },
                 ),
               ),
             ],
           ),
         ),

 

להורדת הקוד אותו פיתחנו במדריך

 

 

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

flutter - קלאסים ווידג'טים

Dart לפיתוח אפליקציות Flutter - ממש על קצה המזלג

 

 

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

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

 

 

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

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

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

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

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

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

 

 

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

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