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

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

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

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

CRUD הם 4 הפעולות שאנחנו עושים על מידע במסד הנתונים. כולל: Create יצירה, Read קריאה, Update עדכון ו-Delete מחיקה. במדריך זה נשתמש במסד נתונים של Firebase כי האינטגרציה בינו לבין Flutter היא הפשוטה ביותר.

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

Demonstration of Flutter app that performs CRUD operations with Firebase as a database layer

 

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

 

את המכוניות ברשימה נוכל לערוך או למחוק Update או Delete:

Flutter app real-time update and delete

במדריך הנוכחי נלמד להוסיף רישומים של מכוניות למסד הנתונים Create. במדריכים הבאים נלמד לעשות את יתר הפעולות במסגרת ה-CRUD.

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

נתחיל ממסד נתונים ריק:

Firebase empty database

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

$ flutter create [project_name]

נתקין את החבילות של Dart הדרושות לנו לעבודה עם מסד הנתונים: firebase_core ו-firebase_database.

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

נייבא את התלויות אל תוך הסקריפט הראשי של האפליקציה:

/lib/main.dart

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

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

/lib/main.dart

void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp();
 runApp(const MyApp());
}

 

המסך הראשי של האפליקציה

האפליקציה שלנו כוללת מסך אחד המוצג למשתמש בתור קלאס מסוג StatefulWidget שנקרא לו "HomeScreen". נוסיף אותו:

lib/screens/home_screen.dart

import 'package:flutter/material.dart';
 
 
class HomeScreen extends StatefulWidget {
 const HomeScreen({Key? key}) : super(key: key);
 
 @override
 State<HomeScreen> createState() => _HomeScreenState();
}
 
class _HomeScreenState extends State<HomeScreen> {
 final _formKey = GlobalKey<FormState>();
 
 final TextEditingController _editNameController = TextEditingController();
 final TextEditingController _editPriceController = TextEditingController();
 
 var _errorMsg = '';
 
 @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: Column(
         children: const [],
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () {},
       child: const Icon(Icons.add),
     ),
   );
 }
}
  • הוספנו 3 ווידג'טים עיקריים: appBar, Column ו-floatingActionButton. לחיצה על כפתור הפעולה המרחף תפתח טופס לתוכו נזין את פרטי המכוניות. בתוך ה-Column נוסיף עוד מעט את הטופס.
  • במשתנה errorMsg נשתמש בהמשך כדי לתקשר הודעות שגיאה למשתמש.
  • המשתנה formKey ישמש לזיהוי הטופס על ידי Flutter. נשתמש בו עוד מעט בשביל ולידציה.
final _formKey = GlobalKey<FormState>();
  • משתנים מסוג TextEditingController יהיו הקונטרולרים של שדות הטופס. שדה ראשון לתוכו יזינו המשתמשים את שם המכונית ושני בשביל המחיר.
final TextEditingController _editNameController = TextEditingController();
 final TextEditingController _editPriceController = TextEditingController();

 

הסקריפט הראשי של האפליקציה main.dart

נגדיר את המסך שזה עתה יצרנו בתור דף הבית home של האפליקציה.

לשם כך, נייבא את הקובץ dart של המסך:

/lib/main.dart

import 'screens/home_screen.dart';

נשלב אותו באפליקציה:

/lib/main.dart

class MyApp extends StatelessWidget {
 const MyApp({super.key});
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
       title: 'My Garage App',
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: const HomeScreen(),
   );
 }
}

 

הטופס לתוכו נזין את פרטי המכוניות

את הטופס לתוכו נזין את פרטי המכוניות נשים בתוך ווידג'ט מסוג Dialog. נוסיף אותו אחרי המתודה build:

lib/screens/home_screen.dart

void _carDialog(String key) {
   // We'll use this to pop the route
   var cachedCtx = context;
 
   _editNameController.text = '';
   _editPriceController.text = '';
 
   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: () {
 
                     },
                     child: const Text("Save"),
                   )
                 ],
               ),
             ),
           ),
         );
       });
 }
  • הפונקציה להצגת הטופס שמה "carDialog" והיא מקבלת מחרוזת key שהיא מזהה הרשומה במסד הנתונים. אם ערך זה ריק הטופס צריך להיות ריק, וישמש להוספת רשומה Create במקום לעדכון Update.
  • הדיאלוג שהוא חלון קופץ מופיע באמצעות הפונקציה showDialog שמכילה בתוכה את הווידג'ט Dialog.
  • בתוך הטופס השדות מסודרים אחד מעל לשני בתוך ווידג'ט מסוג Column. נגדיר :
mainAxisSize: MainAxisSize.min

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

  • בתוך הדיאלוג שמנו ווידג'ט Form הכולל את שני שדות הטופס אותם הגדרנו באמצעות TextFormField . השדות מאפשרים את עריכת שם המכונית ומחיר. השדות כוללים, בין השאר, פונקציות ולידציה והגדרת סוג המקלדת.
  • הוספנו את הקוד:
_editNameController.text = '';
_editPriceController.text = '';

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

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

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

floatingActionButton: FloatingActionButton(
       onPressed: () {
         _carDialog("");
       },
       child: const Icon(Icons.add),
),

 

קלאס לניהול המידע של האפליקציה

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

נפתח סקריפט חדש של dart:

lib/providers/cars_provider.dart

נוסיף קלאס שיתקשר עם מסד הנתונים של Firebase אותו נקים בתוך תיקיית providers:

lib/providers/cars_provider.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
 
class CarsProvider extends ChangeNotifier {
 final DatabaseReference _db = FirebaseDatabase.instance.ref();
 static const carsPath = 'Cars';
 
}
  • הקלאס יורש את ChangeNotifier בגלל שהוא החלק של האפליקציה שמנהל את המידע במסגרת דפוס ה-provider.
  • הקלאס כולל את הרפרנס, המצביע, על מסד הנתונים ואת שם הטבלה במסד הנתונים "Cars" איתה נעבוד.

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

final Map<String, dynamic> data = {
       "name": name.toString(),
       "price": price,
       "isFavorite": false,
       "date": DateTime.now().millisecondsSinceEpoch,
};
 
_db.child(carsPath).push().set(data);

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

lib/providers/cars_provider.dart

Future<bool> add(String name, double price) async {
   try {
     final Map<String, dynamic> 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;
   }
 }

הפונקציה מקבלת את השדות name ו-price, הופכת אותם למפה Map הכוללת את התאריך במילישניות (כי ככה מקובל כשעובדים עם מסד נתונים Realtime של Firebase), ומחזירה ערך בוליאני. true במקרה של הצלחת הרישום או false במקרה של תקלה.

 

שימוש ב-provider לניהול המידע state של האפליקציה

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

ChangeNotifierProvider מאפשר את העברת המידע מהקלאס שמחזיק את המידע, ה-provider, לווידג'טים שמאזינים לו. ה-ChangeNotifierProvider הוא ווידג'ט שעוטף את הווידג'טים שמאזינים למידע, ובו מוגדר הקלאס מספק המידע. מכיוון שכל הווידג'טים של האפליקציה שלנו צריכים את המידע נמקם אותו בראש ה-widget tree בקובץ main.dart.

נייבא את התלויות :

lib/main.dart

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

import 'providers/cars_provider.dart';
import 'screens/home_screen.dart';

נשנה את הקוד באופן שיאפשר עבודה עם provider:

lib/main.dart

class MyApp extends StatelessWidget {
 const MyApp({super.key});
 @override
 Widget build(BuildContext context) {
   return MultiProvider(
     providers: [
       ChangeNotifierProvider(
         create: (context) => CarsProvider(),
       ),
     ],
     child: MaterialApp(
       title: 'My Garage App',
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: const HomeScreen(),
     ),
   );
 }
}
  • MultiProvider מכיל רשימה של קלאסים ספקי מידע, ה-providers. במקרה שלנו מקור המידע הוא הקלאס CarsProvider.
  • בשדה child הגדרנו את הווידג'ט שצריך להאזין למידע שמקורו ב- providers. המאזין שלנו הוא המסך HomeScreen שמכיל את כל הווידג'טים שמתווכים את המידע למשתמשים.

 

הגשת הטופס

נוסיף לטמפלייט את התלויות שיאפשרו לו לעבוד עם ה- provider:

lib/screens/home_screen.dart

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

import '../providers/cars_provider.dart';

נוסיף לטופס את הקוד שיקרא לפונקציה "add" שמקורה בקלאס ה-provider כאשר המשתמש יגיש את הטופס:

lib/screens/home_screen.dart

ElevatedButton(
   onPressed: () async {
       // code goes here
   },
   child: const Text("Save"),
)

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

if (_formKey.currentState!.validate()) {
    // trying adding a record
}
  • ולידציה של Flutter תעשה את העבודה ותציג למשתמש את השגיאות שעשה בהזנת הטופס.

נקלוט את המידע שהזין המשתמש לתוך השדות ונעשה סניטציה (=ננקה את המידע שהזינו המשתמשים):

if (_formKey.currentState!.validate()) {
    final formName = _editNameController.text.trim().toString();
    final formPrice = double.parse(_editPriceController.text.trim());
}
  • בשלב הסניטציה נהפוך את השדה "שם המכונית" למחרוזת String ולשדה "מחיר" נעשה פרסינג ל-float.

נוסיף קריאה לפונקציה "add" באמצעות ה- provider:

if (_formKey.currentState!.validate()) {
    final formName = _editNameController.text.toString();
    final formPrice = double.parse(_editPriceController.text);
 
    Provider.of<CarsProvider>(context, listen: false)
                             .add(formName, formPrice);
 
}

הפונקציה היא א-סינכרונית מה שמאפשר לנו להוסיף לה callback בו נגדיר את התנהגות האפליקציה בתגובה למידע המוחזר מהמתודה בקלאס (זוכרים, התגובה היא בוליאנית. הפונקציה מחזירה true או false):

Provider.of<CarsProvider>(context, listen: false)
  .add(formName, formPrice)
  .then((success) {
    if (success) {
      if (!mounted) return;
        Navigator.of(cachedCtx).pop();
       } else {
          _errorMsg = 'Problem adding a new car';
    }
});
  • את הערך שמחזירה הפונקציה נציב לערך המשתנה "success" בעל הערך הבוליאני, true או false.
  • במקרה של הצלחת הפעולה, הקוד:
Navigator.of(cachedCtx).pop(); 

יחזיר אותנו ל-route ממנו הגענו ולמעשה יסגור את הדיאלוג.

  • במקרה של בעיה תוצג למשתמש הודעת שגיאה.

כך יראה הטופס במקרה בו שגה המשתמש בהזנת הטופס:

In the case of an invalid form fields flutter shows the validation errors under the fields

כך נראה הווידג'ט של כפתור שליחת הטופס לאחר שהוספנו לו קוד לטיפול בשליחת הטופס:

lib/screens/home_screen.dart

ElevatedButton(
 onPressed: () {
   if (_formKey.currentState!.validate()) {
     final formName =
         _editNameController.text.trim().toString();
     final formPrice =
         double.parse(_editPriceController.text.trim());
 
     Provider.of<CarsProvider>(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"),
)

כך יראה מסד הנתונים של Firebase אחרי שהזנו לתוכו רשומה באמצעות האפליקציה:

The firebase real-time database once the db was filled with the first records

 

לסיכום

לסיכום, להלן הקוד המלא של קבצי ה-dart של האפליקציה.

הקלאס CarsProvider:

providers/cars_provider.dart

import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
 
class CarsProvider extends ChangeNotifier {
 final DatabaseReference _db = FirebaseDatabase.instance.ref();
 static const carsPath = 'Cars';
 
 Future<bool> add(String name, double price) async {
   try {
     final Map<String, dynamic> 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;
   }
 }
}

הקובץ הראשי של האפליקציה:

lib/main.dart

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

import 'providers/cars_provider.dart';
import 'screens/home_screen.dart';
 
void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp();
 runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
 const MyApp({super.key});
 @override
 Widget build(BuildContext context) {
    return MultiProvider(
     providers: [
       ChangeNotifierProvider(
         create: (context) => CarsProvider(),
       ),
     ],
     child: MaterialApp(
       title: 'My Garage App',
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: const HomeScreen(),
     ),
   );
 }
}

המסך, home_screen.dart:

screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
 
import '../providers/cars_provider.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 = '';
 
 @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: Column(
         children: const [],
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         _carDialog("");
       },
       child: const Icon(Icons.add),
     ),
   );
 }
 
 void _carDialog(String key) {
   _editNameController.text = '';
   _editPriceController.text = '';
   var cachedCtx = context;
   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 הוא "CRUD באפליקציית Flutter - מדריך שני : קריאה ממסד הנתונים" ובו נלמד כיצד לקרוא ממסד נתונים באופן שהמידע המתעדכן במסד הנתונים יעדכן אוטומטית את תצוגת האפליקציה.

 

אם הגעת עד לפה כנראה שגם זה יעניין אותך:

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

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

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

 

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

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

 

 

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

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

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

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

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

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

 

 

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

דג למים הוא כמו ציפור ל...?