ساخت اپلیکیشن فلاتر ToDo با SQLite — از صفر تا صد

دادهها برای کاربران حائز اهمیت بالایی است و از این رو برای آنها راحت نیست که بخواهند به طور مرتب دادههای تکراری را وارد کنند و یا مکرراً منتظر بمانند تا دادههای یکسانی از اینترنت بارگذاری شوند. در چنین مواردی بهتر است دادهها را به صورت محلی ذخیره کنیم. در این مقاله مراحل ساخت اپلیکیشن فلاتر ToDo با پایگاه داده لوکال SQLite توضیح داده میشود. به این منظور از پلاگین sqflite استفاده میکنیم.
اهدافی که در این مطلب دنبال میکنیم عبارت هستند از:
- ایجاد لیست ToDo
- مدیریت عملیات CRUD برای ذخیره و بازیابی دادهها
اپلیکیشنی که میخواهیم بسازیم شبیه به تصویر زیر است:
گام 1
قبل از هر چیز باید کلاس مدل را برای آیتم ToDo ایجاد کنیم. ما از عنوان، توضیح، تاریخ و id برای هر آیتم To-Do استفاده میکنیم. یک کلاس مدل به صورت todo.dart ایجاد کرده و کد زیر را در آن قرار میدهیم:
class Todo { int _id; String _title; String _description; String _date; Todo(this._title, this._date, [this._description] ); Todo.withId(this._id, this._title, this._date, [this._description]); int get id => _id; String get title => _title; String get description => _description; String get date => _date; set title(String newTitle) { if (newTitle.length <= 255) { this._title = newTitle; } } set description(String newDescription) { if (newDescription.length <= 255) { this._description = newDescription; } } set date(String newDate) { this._date = newDate; } // Convert a Note object into a Map object Map<String, dynamic> toMap() { var map = Map<String, dynamic>(); if (id != null) { map['id'] = _id; } map['title'] = _title; map['description'] = _description; map['date'] = _date; return map; } // Extract a Note object from a Map object Todo.fromMapObject(Map<String, dynamic> map) { this._id = map['id']; this._title = map['title']; this._description = map['description']; this._date = map['date']; } }
گام 2
در این مرحله باید عملیات CRUD پایگاه داده SQLite را پیادهسازی کنیم. به این منظور یک کلاس مجزا ایجاد کرده و همه عملیات درج، بهروزرسانی و حذف را با ایجاد جداولی پیادهسازی میکنیم. پیش از آن باید وابستگیها را اضافه کنیم تا بتوانیم از SQLite در پروژه خود استفاده کنیم. به این منظور به فایل pubspec.yaml بروید و وابستگیهای زیر را به پروژه اضافه کرده و آن را ذخیره کنید:
dependencies: flutter: sdk: flutter sqflite: any path_provider: any intl: ^0.15.7
در ادامه برخی تابعهای مهم را در کلاس database_helper مشاهده میکنید:
Future<Database> get database async { if (_database == null) { _database = await initializeDatabase(); } return _database; }
تابع فوق شیء پایگاه داده را ایجاد کرده و یک getter در آن ارائه میکند که در صورت عدم ایجاد وهلهای از پایگاه داده، از آن برای وهلهسازی پایگاه داده استفاده میکنیم. این کار «مقداردهی با تأخیر» (Lazy Initialization) نامیده میشود.
Future<Database> initializeDatabase() async { Directory directory = await getApplicationDocumentsDirectory(); String path = directory.path + 'todos.db'; var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb); return todosDatabase; }
اگر هیچ شیئی به پایگاه داده انتساب نیافته باشد، از تابع initializeDatabase برای ایجاد پایگاه داده بهره میگیریم. در این تابع مسیر ذخیرهسازی پایگاه داده و ایجاد جداول مطلوب را به دست خواهیم آورد. نام پایگاه داده را todos تعیین میکنیم:
void _createDb(Database db, int newVersion) async { await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, ' '$colDescription TEXT, $colDate TEXT)'); }
سپس جدولها را مانند کد فوق ایجاد میکنیم. در ادامه باید تابعهای درج، بهروزرسانی و حذف را اضافه کنیم. در ادامه کد کامل مربوط به کلاس database_helper را میبینید:
import 'package:sqflite/sqflite.dart'; import 'dart:async'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:todo_list/Models/todo.dart'; class DatabaseHelper { static DatabaseHelper _databaseHelper; // Singleton DatabaseHelper static Database _database; // Singleton Database String todoTable = 'todo_table'; String colId = 'id'; String colTitle = 'title'; String colDescription = 'description'; String colDate = 'date'; DatabaseHelper._createInstance(); // Named constructor to create instance of DatabaseHelper factory DatabaseHelper() { if (_databaseHelper == null) { _databaseHelper = DatabaseHelper._createInstance(); // This is executed only once, singleton object } return _databaseHelper; } Future<Database> get database async { if (_database == null) { _database = await initializeDatabase(); } return _database; } Future<Database> initializeDatabase() async { // Get the directory path for both Android and iOS to store database. Directory directory = await getApplicationDocumentsDirectory(); String path = directory.path + 'todos.db'; // Open/create the database at a given path var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb); return todosDatabase; } void _createDb(Database db, int newVersion) async { await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, ' '$colDescription TEXT, $colDate TEXT)'); } // Fetch Operation: Get all todo objects from database Future<List<Map<String, dynamic>>> getTodoMapList() async { Database db = await this.database; // var result = await db.rawQuery('SELECT * FROM $todoTable order by $colTitle ASC'); var result = await db.query(todoTable, orderBy: '$colTitle ASC'); return result; } // Insert Operation: Insert a todo object to database Future<int> insertTodo(Todo todo) async { Database db = await this.database; var result = await db.insert(todoTable, todo.toMap()); return result; } // Update Operation: Update a todo object and save it to database Future<int> updateTodo(Todo todo) async { var db = await this.database; var result = await db.update(todoTable, todo.toMap(), where: '$colId = ?', whereArgs: [todo.id]); return result; } // Delete Operation: Delete a todo object from database Future<int> deleteTodo(int id) async { var db = await this.database; int result = await db.rawDelete('DELETE FROM $todoTable WHERE $colId = $id'); return result; } // Get number of todo objects in database Future<int> getCount() async { Database db = await this.database; List<Map<String, dynamic>> x = await db.rawQuery('SELECT COUNT (*) from $todoTable'); int result = Sqflite.firstIntValue(x); return result; } // Get the 'Map List' [ List<Map> ] and convert it to 'todo List' [ List<Todo> ] Future<List<Todo>> getTodoList() async { var todoMapList = await getTodoMapList(); // Get 'Map List' from database int count = todoMapList.length; // Count the number of map entries in db table List<Todo> todoList = List<Todo>(); // For loop to create a 'todo List' from a 'Map List' for (int i = 0; i < count; i++) { todoList.add(Todo.fromMapObject(todoMapList[i])); } return todoList; } }
گام 3
اکنون باید صفحهها را برای لیست ToDo پیادهسازی کنیم. یک پوشه به نام Screen ایجاد کرده و فایل todo_list.dart را اضافه میکنیم.
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:todo_list/Models/todo.dart'; import 'package:todo_list/Utils/database_helper.dart'; import 'package:todo_list/Screens/todo_detail.dart'; import 'package:sqflite/sqflite.dart'; class TodoList extends StatefulWidget { @override State<StatefulWidget> createState() { return TodoListState(); } } class TodoListState extends State<TodoList> { DatabaseHelper databaseHelper = DatabaseHelper(); List<Todo> todoList; int count = 0; @override Widget build(BuildContext context) { if (todoList == null) { todoList = List<Todo>(); updateListView(); } return Scaffold( appBar: AppBar( title: Text('Todos'), ), body: getTodoListView(), floatingActionButton: FloatingActionButton( onPressed: () { debugPrint('FAB clicked'); navigateToDetail(Todo('', '', ''), 'Add Todo'); }, tooltip: 'Add Todo', child: Icon(Icons.add), ), ); } ListView getTodoListView() { return ListView.builder( itemCount: count, itemBuilder: (BuildContext context, int position) { return Card( color: Colors.white, elevation: 2.0, child: ListTile( leading: CircleAvatar( backgroundColor: Colors.amber, child: Text(getFirstLetter(this.todoList[position].title), style: TextStyle(fontWeight: FontWeight.bold)), ), title: Text(this.todoList[position].title, style: TextStyle(fontWeight: FontWeight.bold)), subtitle: Text(this.todoList[position].description), trailing: Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ GestureDetector( child: Icon(Icons.delete,color: Colors.red,), onTap: () { _delete(context, todoList[position]); }, ), ], ), onTap: () { debugPrint("ListTile Tapped"); navigateToDetail(this.todoList[position], 'Edit Todo'); }, ), ); }, ); } getFirstLetter(String title) { return title.substring(0, 2); } void _delete(BuildContext context, Todo todo) async { int result = await databaseHelper.deleteTodo(todo.id); if (result != 0) { _showSnackBar(context, 'Todo Deleted Successfully'); updateListView(); } } void _showSnackBar(BuildContext context, String message) { final snackBar = SnackBar(content: Text(message)); Scaffold.of(context).showSnackBar(snackBar); } void navigateToDetail(Todo todo, String title) async { bool result = await Navigator.push(context, MaterialPageRoute(builder: (context) { return TodoDetail(todo, title); })); if (result == true) { updateListView(); } } void updateListView() { final Future<Database> dbFuture = databaseHelper.initializeDatabase(); dbFuture.then((database) { Future<List<Todo>> todoListFuture = databaseHelper.getTodoList(); todoListFuture.then((todoList) { setState(() { this.todoList = todoList; this.count = todoList.length; }); }); }); } }
در کد فوق، یک «نمای لیست» (List View) پیادهسازی میکنیم. در این نمای لیست، ToDo-هایی که وارد شدهاند نمایش مییابند. بنابراین در حال حاضر باید ToDo-ها را در پایگاه داده وارد کنیم. این کار با استفاده از کد زیر انجام مییابد. صفحه دیگری نیز برای افزودن ToDo-ها ایجاد میکنیم.
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:todo_list/Models/todo.dart'; import 'package:todo_list/Utils/database_helper.dart'; import 'package:intl/intl.dart'; class TodoDetail extends StatefulWidget { final String appBarTitle; final Todo todo; TodoDetail(this.todo, this.appBarTitle); @override State<StatefulWidget> createState() { return TodoDetailState(this.todo, this.appBarTitle); } } class TodoDetailState extends State<TodoDetail> { DatabaseHelper helper = DatabaseHelper(); String appBarTitle; Todo todo; TextEditingController titleController = TextEditingController(); TextEditingController descriptionController = TextEditingController(); TodoDetailState(this.todo, this.appBarTitle); @override Widget build(BuildContext context) { TextStyle textStyle = Theme.of(context).textTheme.title; titleController.text = todo.title; descriptionController.text = todo.description; return WillPopScope( onWillPop: () { moveToLastScreen(); }, child: Scaffold( appBar: AppBar( title: Text(appBarTitle), leading: IconButton(icon: Icon( Icons.arrow_back), onPressed: () { moveToLastScreen(); } ), ), body: Padding( padding: EdgeInsets.only(top: 15.0, left: 10.0, right: 10.0), child: ListView( children: <Widget>[ Padding( padding: EdgeInsets.only(top: 15.0, bottom: 15.0), child: TextField( controller: titleController, style: textStyle, onChanged: (value) { debugPrint('Something changed in Title Text Field'); updateTitle(); }, decoration: InputDecoration( labelText: 'Title', labelStyle: textStyle, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0) ) ), ), ), Padding( padding: EdgeInsets.only(top: 15.0, bottom: 15.0), child: TextField( controller: descriptionController, style: textStyle, onChanged: (value) { debugPrint('Something changed in Description Text Field'); updateDescription(); }, decoration: InputDecoration( labelText: 'Description', labelStyle: textStyle, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0) ) ), ), ), Padding( padding: EdgeInsets.only(top: 15.0, bottom: 15.0), child: Row( children: <Widget>[ Expanded( child: RaisedButton( color: Theme.of(context).primaryColorDark, textColor: Theme.of(context).primaryColorLight, child: Text( 'Save', textScaleFactor: 1.5, ), onPressed: () { setState(() { debugPrint("Save button clicked"); _save(); }); }, ), ), Container(width: 5.0,), Expanded( child: RaisedButton( color: Theme.of(context).primaryColorDark, textColor: Theme.of(context).primaryColorLight, child: Text( 'Delete', textScaleFactor: 1.5, ), onPressed: () { setState(() { debugPrint("Delete button clicked"); _delete(); }); }, ), ), ], ), ), ], ), ), )); } void moveToLastScreen() { Navigator.pop(context, true); } // Update the title of todo object void updateTitle(){ todo.title = titleController.text; } // Update the description of todo object void updateDescription() { todo.description = descriptionController.text; } // Save data to database void _save() async { moveToLastScreen(); todo.date = DateFormat.yMMMd().format(DateTime.now()); int result; if (todo.id != null) { // Case 1: Update operation result = await helper.updateTodo(todo); } else { // Case 2: Insert Operation result = await helper.insertTodo(todo); } if (result != 0) { // Success _showAlertDialog('Status', 'Todo Saved Successfully'); } else { // Failure _showAlertDialog('Status', 'Problem Saving Todo'); } } void _delete() async { moveToLastScreen(); if (todo.id == null) { _showAlertDialog('Status', 'No Todo was deleted'); return; } int result = await helper.deleteTodo(todo.id); if (result != 0) { _showAlertDialog('Status', 'Todo Deleted Successfully'); } else { _showAlertDialog('Status', 'Error Occured while Deleting Todo'); } } void _showAlertDialog(String title, String message) { AlertDialog alertDialog = AlertDialog( title: Text(title), content: Text(message), ); showDialog( context: context, builder: (_) => alertDialog ); } }
اینک کار طراحی اپلیکیشن به پایان رسیده است. ترمینال را باز کرده و پس از اتصال گوشی از طریق USB، دستور زیر را اجرا کنید:
flutter run
نتیجه
سخن پایانی
در این راهنما با شیوه استفاده از ویجتهای فلاتر و پیادهسازی آن در یک اپلیکیشن ToDo آشنا شدید. علاوه بر آن با شیوه افزودن دادهها در پایگاه داده لوکال در فلاتر را نیز فرا گرفتید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای پایگاه داده
- گوگل فلاتر (Flutter) از صفر تا صد — ساخت اپلیکیشن به کمک ویجت
- فلاتر برای وب — راهنمای مقدماتی
- مفاهیم مقدماتی فلاتر (Flutter) — به زبان ساده
==