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

۳۰۱ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۷ دقیقه
ساخت اپلیکیشن فلاتر ToDo با SQLite — از صفر تا صد

داده‌ها برای کاربران حائز اهمیت بالایی است و از این رو برای آن‌ها راحت نیست که بخواهند به طور مرتب داده‌های تکراری را وارد کنند و یا مکرراً منتظر بمانند تا داده‌های یکسانی از اینترنت بارگذاری شوند. در چنین مواردی بهتر است داده‌ها را به صورت محلی ذخیره کنیم. در این مقاله مراحل ساخت اپلیکیشن فلاتر ToDo با پایگاه داده لوکال SQLite توضیح داده می‌شود. به این منظور از پلاگین sqflite استفاده می‌کنیم.

فهرست مطالب این نوشته

اهدافی که در این مطلب دنبال می‌کنیم عبارت هستند از:

  • ایجاد لیست ToDo
  • مدیریت عملیات CRUD برای ذخیره و بازیابی داده‌ها

اپلیکیشنی که می‌خواهیم بسازیم شبیه به تصویر زیر است:

ساخت اپلیکیشن فلاتر ToDo

گام 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

ساخت اپلیکیشن فلاتر ToDo

سخن پایانی

در این راهنما با شیوه استفاده از ویجت‌های فلاتر و پیاده‌سازی آن در یک اپلیکیشن ToDo آشنا شدید. علاوه بر آن با شیوه افزودن داده‌ها در پایگاه داده لوکال در فلاتر را نیز فرا گرفتید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
abeythilakeudara3
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *