ساخت اپلیکیشن فلاتر ToDo با SQLite — از صفر تا صد
دادهها برای کاربران حائز اهمیت بالایی است و از این رو برای آنها راحت نیست که بخواهند به طور مرتب دادههای تکراری را وارد کنند و یا مکرراً منتظر بمانند تا دادههای یکسانی از اینترنت بارگذاری شوند. در چنین مواردی بهتر است دادهها را به صورت محلی ذخیره کنیم. در این مقاله مراحل ساخت اپلیکیشن فلاتر ToDo با پایگاه داده لوکال SQLite توضیح داده میشود. به این منظور از پلاگین sqflite استفاده میکنیم.
اهدافی که در این مطلب دنبال میکنیم عبارت هستند از:
- ایجاد لیست ToDo
- مدیریت عملیات CRUD برای ذخیره و بازیابی دادهها
اپلیکیشنی که میخواهیم بسازیم شبیه به تصویر زیر است:
گام 1
قبل از هر چیز باید کلاس مدل را برای آیتم ToDo ایجاد کنیم. ما از عنوان، توضیح، تاریخ و id برای هر آیتم To-Do استفاده میکنیم.
یک کلاس مدل به صورت todo.dart ایجاد کرده و کد زیر را در آن قرار میدهیم:
1class Todo {
2
3 int _id;
4 String _title;
5 String _description;
6 String _date;
7
8 Todo(this._title, this._date, [this._description] );
9
10 Todo.withId(this._id, this._title, this._date, [this._description]);
11
12 int get id => _id;
13
14 String get title => _title;
15
16 String get description => _description;
17
18 String get date => _date;
19
20
21 set title(String newTitle) {
22 if (newTitle.length <= 255) {
23 this._title = newTitle;
24 }
25 }
26 set description(String newDescription) {
27 if (newDescription.length <= 255) {
28 this._description = newDescription;
29 }
30 }
31
32 set date(String newDate) {
33 this._date = newDate;
34 }
35
36 // Convert a Note object into a Map object
37 Map<String, dynamic> toMap() {
38
39 var map = Map<String, dynamic>();
40 if (id != null) {
41 map['id'] = _id;
42 }
43 map['title'] = _title;
44 map['description'] = _description;
45 map['date'] = _date;
46
47 return map;
48 }
49
50 // Extract a Note object from a Map object
51 Todo.fromMapObject(Map<String, dynamic> map) {
52 this._id = map['id'];
53 this._title = map['title'];
54 this._description = map['description'];
55 this._date = map['date'];
56 }
57}
گام 2
در این مرحله باید عملیات CRUD پایگاه داده SQLite را پیادهسازی کنیم. به این منظور یک کلاس مجزا ایجاد کرده و همه عملیات درج، بهروزرسانی و حذف را با ایجاد جداولی پیادهسازی میکنیم. پیش از آن باید وابستگیها را اضافه کنیم تا بتوانیم از SQLite در پروژه خود استفاده کنیم. به این منظور به فایل pubspec.yaml بروید و وابستگیهای زیر را به پروژه اضافه کرده و آن را ذخیره کنید:
1dependencies:
2 flutter:
3 sdk: flutter
4 sqflite: any
5 path_provider: any
6 intl: ^0.15.7
در ادامه برخی تابعهای مهم را در کلاس database_helper مشاهده میکنید:
1Future<Database> get database async {
2
3 if (_database == null) {
4 _database = await initializeDatabase();
5 }
6 return _database;
7 }
تابع فوق شیء پایگاه داده را ایجاد کرده و یک getter در آن ارائه میکند که در صورت عدم ایجاد وهلهای از پایگاه داده، از آن برای وهلهسازی پایگاه داده استفاده میکنیم. این کار «مقداردهی با تأخیر» (Lazy Initialization) نامیده میشود.
1 Future<Database> initializeDatabase() async {
2
3 Directory directory = await getApplicationDocumentsDirectory();
4 String path = directory.path + 'todos.db';
5 var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb);
6 return todosDatabase;
7 }
اگر هیچ شیئی به پایگاه داده انتساب نیافته باشد، از تابع initializeDatabase برای ایجاد پایگاه داده بهره میگیریم. در این تابع مسیر ذخیرهسازی پایگاه داده و ایجاد جداول مطلوب را به دست خواهیم آورد. نام پایگاه داده را todos تعیین میکنیم:
1 void _createDb(Database db, int newVersion) async {
2
3 await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, '
4 '$colDescription TEXT, $colDate TEXT)');
5 }
سپس جدولها را مانند کد فوق ایجاد میکنیم. در ادامه باید تابعهای درج، بهروزرسانی و حذف را اضافه کنیم. در ادامه کد کامل مربوط به کلاس database_helper را میبینید:
1import 'package:sqflite/sqflite.dart';
2import 'dart:async';
3import 'dart:io';
4import 'package:path_provider/path_provider.dart';
5import 'package:todo_list/Models/todo.dart';
6
7class DatabaseHelper {
8
9 static DatabaseHelper _databaseHelper; // Singleton DatabaseHelper
10 static Database _database; // Singleton Database
11
12 String todoTable = 'todo_table';
13 String colId = 'id';
14 String colTitle = 'title';
15 String colDescription = 'description';
16 String colDate = 'date';
17
18 DatabaseHelper._createInstance(); // Named constructor to create instance of DatabaseHelper
19
20 factory DatabaseHelper() {
21
22 if (_databaseHelper == null) {
23 _databaseHelper = DatabaseHelper._createInstance(); // This is executed only once, singleton object
24 }
25 return _databaseHelper;
26 }
27
28 Future<Database> get database async {
29
30 if (_database == null) {
31 _database = await initializeDatabase();
32 }
33 return _database;
34 }
35
36 Future<Database> initializeDatabase() async {
37 // Get the directory path for both Android and iOS to store database.
38 Directory directory = await getApplicationDocumentsDirectory();
39 String path = directory.path + 'todos.db';
40
41 // Open/create the database at a given path
42 var todosDatabase = await openDatabase(path, version: 1, onCreate: _createDb);
43 return todosDatabase;
44 }
45
46 void _createDb(Database db, int newVersion) async {
47
48 await db.execute('CREATE TABLE $todoTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colTitle TEXT, '
49 '$colDescription TEXT, $colDate TEXT)');
50 }
51
52 // Fetch Operation: Get all todo objects from database
53 Future<List<Map<String, dynamic>>> getTodoMapList() async {
54 Database db = await this.database;
55
56// var result = await db.rawQuery('SELECT * FROM $todoTable order by $colTitle ASC');
57 var result = await db.query(todoTable, orderBy: '$colTitle ASC');
58 return result;
59 }
60
61 // Insert Operation: Insert a todo object to database
62 Future<int> insertTodo(Todo todo) async {
63 Database db = await this.database;
64 var result = await db.insert(todoTable, todo.toMap());
65 return result;
66 }
67
68 // Update Operation: Update a todo object and save it to database
69 Future<int> updateTodo(Todo todo) async {
70 var db = await this.database;
71 var result = await db.update(todoTable, todo.toMap(), where: '$colId = ?', whereArgs: [todo.id]);
72 return result;
73 }
74
75
76 // Delete Operation: Delete a todo object from database
77 Future<int> deleteTodo(int id) async {
78 var db = await this.database;
79 int result = await db.rawDelete('DELETE FROM $todoTable WHERE $colId = $id');
80 return result;
81 }
82
83 // Get number of todo objects in database
84 Future<int> getCount() async {
85 Database db = await this.database;
86 List<Map<String, dynamic>> x = await db.rawQuery('SELECT COUNT (*) from $todoTable');
87 int result = Sqflite.firstIntValue(x);
88 return result;
89 }
90
91 // Get the 'Map List' [ List<Map> ] and convert it to 'todo List' [ List<Todo> ]
92 Future<List<Todo>> getTodoList() async {
93
94 var todoMapList = await getTodoMapList(); // Get 'Map List' from database
95 int count = todoMapList.length; // Count the number of map entries in db table
96
97 List<Todo> todoList = List<Todo>();
98 // For loop to create a 'todo List' from a 'Map List'
99 for (int i = 0; i < count; i++) {
100 todoList.add(Todo.fromMapObject(todoMapList[i]));
101 }
102
103 return todoList;
104 }
105
106}
گام 3
اکنون باید صفحهها را برای لیست ToDo پیادهسازی کنیم. یک پوشه به نام Screen ایجاد کرده و فایل todo_list.dart را اضافه میکنیم.
1import 'dart:async';
2import 'package:flutter/material.dart';
3import 'package:todo_list/Models/todo.dart';
4import 'package:todo_list/Utils/database_helper.dart';
5import 'package:todo_list/Screens/todo_detail.dart';
6import 'package:sqflite/sqflite.dart';
7
8class TodoList extends StatefulWidget {
9 @override
10 State<StatefulWidget> createState() {
11 return TodoListState();
12 }
13}
14
15class TodoListState extends State<TodoList> {
16 DatabaseHelper databaseHelper = DatabaseHelper();
17 List<Todo> todoList;
18 int count = 0;
19
20 @override
21 Widget build(BuildContext context) {
22 if (todoList == null) {
23 todoList = List<Todo>();
24 updateListView();
25 }
26
27 return Scaffold(
28 appBar: AppBar(
29 title: Text('Todos'),
30 ),
31 body: getTodoListView(),
32 floatingActionButton: FloatingActionButton(
33 onPressed: () {
34 debugPrint('FAB clicked');
35 navigateToDetail(Todo('', '', ''), 'Add Todo');
36 },
37 tooltip: 'Add Todo',
38 child: Icon(Icons.add),
39 ),
40 );
41 }
42
43 ListView getTodoListView() {
44 return ListView.builder(
45 itemCount: count,
46 itemBuilder: (BuildContext context, int position) {
47 return Card(
48 color: Colors.white,
49 elevation: 2.0,
50 child: ListTile(
51 leading: CircleAvatar(
52 backgroundColor: Colors.amber,
53 child: Text(getFirstLetter(this.todoList[position].title),
54 style: TextStyle(fontWeight: FontWeight.bold)),
55 ),
56 title: Text(this.todoList[position].title,
57 style: TextStyle(fontWeight: FontWeight.bold)),
58 subtitle: Text(this.todoList[position].description),
59 trailing: Row(
60 mainAxisSize: MainAxisSize.min,
61 children: <Widget>[
62 GestureDetector(
63 child: Icon(Icons.delete,color: Colors.red,),
64 onTap: () {
65 _delete(context, todoList[position]);
66 },
67 ),
68 ],
69 ),
70 onTap: () {
71 debugPrint("ListTile Tapped");
72 navigateToDetail(this.todoList[position], 'Edit Todo');
73 },
74 ),
75 );
76 },
77 );
78 }
79
80 getFirstLetter(String title) {
81 return title.substring(0, 2);
82 }
83
84 void _delete(BuildContext context, Todo todo) async {
85 int result = await databaseHelper.deleteTodo(todo.id);
86 if (result != 0) {
87 _showSnackBar(context, 'Todo Deleted Successfully');
88 updateListView();
89 }
90 }
91
92 void _showSnackBar(BuildContext context, String message) {
93 final snackBar = SnackBar(content: Text(message));
94 Scaffold.of(context).showSnackBar(snackBar);
95 }
96
97 void navigateToDetail(Todo todo, String title) async {
98 bool result =
99 await Navigator.push(context, MaterialPageRoute(builder: (context) {
100 return TodoDetail(todo, title);
101 }));
102
103 if (result == true) {
104 updateListView();
105 }
106 }
107
108 void updateListView() {
109 final Future<Database> dbFuture = databaseHelper.initializeDatabase();
110 dbFuture.then((database) {
111 Future<List<Todo>> todoListFuture = databaseHelper.getTodoList();
112 todoListFuture.then((todoList) {
113 setState(() {
114 this.todoList = todoList;
115 this.count = todoList.length;
116 });
117 });
118 });
119 }
120
121
122}
در کد فوق، یک «نمای لیست» (List View) پیادهسازی میکنیم. در این نمای لیست، ToDo-هایی که وارد شدهاند نمایش مییابند. بنابراین در حال حاضر باید ToDo-ها را در پایگاه داده وارد کنیم. این کار با استفاده از کد زیر انجام مییابد. صفحه دیگری نیز برای افزودن ToDo-ها ایجاد میکنیم.
1import 'dart:async';
2import 'package:flutter/material.dart';
3import 'package:todo_list/Models/todo.dart';
4import 'package:todo_list/Utils/database_helper.dart';
5import 'package:intl/intl.dart';
6
7class TodoDetail extends StatefulWidget {
8
9 final String appBarTitle;
10 final Todo todo;
11
12 TodoDetail(this.todo, this.appBarTitle);
13
14 @override
15 State<StatefulWidget> createState() {
16
17 return TodoDetailState(this.todo, this.appBarTitle);
18 }
19}
20
21class TodoDetailState extends State<TodoDetail> {
22
23 DatabaseHelper helper = DatabaseHelper();
24
25 String appBarTitle;
26 Todo todo;
27
28 TextEditingController titleController = TextEditingController();
29 TextEditingController descriptionController = TextEditingController();
30
31 TodoDetailState(this.todo, this.appBarTitle);
32
33 @override
34 Widget build(BuildContext context) {
35
36 TextStyle textStyle = Theme.of(context).textTheme.title;
37
38 titleController.text = todo.title;
39 descriptionController.text = todo.description;
40
41 return WillPopScope(
42
43 onWillPop: () {
44 moveToLastScreen();
45 },
46
47 child: Scaffold(
48 appBar: AppBar(
49 title: Text(appBarTitle),
50 leading: IconButton(icon: Icon(
51 Icons.arrow_back),
52 onPressed: () {
53 moveToLastScreen();
54 }
55 ),
56 ),
57
58 body: Padding(
59 padding: EdgeInsets.only(top: 15.0, left: 10.0, right: 10.0),
60 child: ListView(
61 children: <Widget>[
62
63 Padding(
64 padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
65 child: TextField(
66 controller: titleController,
67 style: textStyle,
68 onChanged: (value) {
69 debugPrint('Something changed in Title Text Field');
70 updateTitle();
71 },
72 decoration: InputDecoration(
73 labelText: 'Title',
74 labelStyle: textStyle,
75 border: OutlineInputBorder(
76 borderRadius: BorderRadius.circular(5.0)
77 )
78 ),
79 ),
80 ),
81
82 Padding(
83 padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
84 child: TextField(
85 controller: descriptionController,
86 style: textStyle,
87 onChanged: (value) {
88 debugPrint('Something changed in Description Text Field');
89 updateDescription();
90 },
91 decoration: InputDecoration(
92 labelText: 'Description',
93 labelStyle: textStyle,
94 border: OutlineInputBorder(
95 borderRadius: BorderRadius.circular(5.0)
96 )
97 ),
98 ),
99 ),
100
101 Padding(
102 padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
103 child: Row(
104 children: <Widget>[
105 Expanded(
106 child: RaisedButton(
107 color: Theme.of(context).primaryColorDark,
108 textColor: Theme.of(context).primaryColorLight,
109 child: Text(
110 'Save',
111 textScaleFactor: 1.5,
112 ),
113 onPressed: () {
114 setState(() {
115 debugPrint("Save button clicked");
116 _save();
117 });
118 },
119 ),
120 ),
121
122 Container(width: 5.0,),
123
124 Expanded(
125 child: RaisedButton(
126 color: Theme.of(context).primaryColorDark,
127 textColor: Theme.of(context).primaryColorLight,
128 child: Text(
129 'Delete',
130 textScaleFactor: 1.5,
131 ),
132 onPressed: () {
133 setState(() {
134 debugPrint("Delete button clicked");
135 _delete();
136 });
137 },
138 ),
139 ),
140
141 ],
142 ),
143 ),
144
145
146 ],
147 ),
148 ),
149
150 ));
151 }
152
153 void moveToLastScreen() {
154 Navigator.pop(context, true);
155 }
156
157 // Update the title of todo object
158 void updateTitle(){
159 todo.title = titleController.text;
160 }
161
162 // Update the description of todo object
163 void updateDescription() {
164 todo.description = descriptionController.text;
165 }
166
167 // Save data to database
168 void _save() async {
169
170 moveToLastScreen();
171
172 todo.date = DateFormat.yMMMd().format(DateTime.now());
173 int result;
174 if (todo.id != null) { // Case 1: Update operation
175 result = await helper.updateTodo(todo);
176 } else { // Case 2: Insert Operation
177 result = await helper.insertTodo(todo);
178 }
179
180 if (result != 0) { // Success
181 _showAlertDialog('Status', 'Todo Saved Successfully');
182 } else { // Failure
183 _showAlertDialog('Status', 'Problem Saving Todo');
184 }
185
186 }
187
188
189 void _delete() async {
190
191 moveToLastScreen();
192
193 if (todo.id == null) {
194 _showAlertDialog('Status', 'No Todo was deleted');
195 return;
196 }
197
198 int result = await helper.deleteTodo(todo.id);
199 if (result != 0) {
200 _showAlertDialog('Status', 'Todo Deleted Successfully');
201 } else {
202 _showAlertDialog('Status', 'Error Occured while Deleting Todo');
203 }
204 }
205
206 void _showAlertDialog(String title, String message) {
207
208 AlertDialog alertDialog = AlertDialog(
209 title: Text(title),
210 content: Text(message),
211 );
212 showDialog(
213 context: context,
214 builder: (_) => alertDialog
215 );
216 }
217
218}
اینک کار طراحی اپلیکیشن به پایان رسیده است. ترمینال را باز کرده و پس از اتصال گوشی از طریق USB، دستور زیر را اجرا کنید:
flutter run
نتیجه
سخن پایانی
در این راهنما با شیوه استفاده از ویجتهای فلاتر و پیادهسازی آن در یک اپلیکیشن ToDo آشنا شدید. علاوه بر آن با شیوه افزودن دادهها در پایگاه داده لوکال در فلاتر را نیز فرا گرفتید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای پایگاه داده
- گوگل فلاتر (Flutter) از صفر تا صد — ساخت اپلیکیشن به کمک ویجت
- فلاتر برای وب — راهنمای مقدماتی
- مفاهیم مقدماتی فلاتر (Flutter) — به زبان ساده
==