Picture of the author

Sử dụng Flutter, cơ sở dữ liệu SQLite tạo ứng dụng To Do List

Xây dựng ứng dụng quốc dân với công nghệ đa nền tảng mới - Flutter, dựa trên ngôn ngữ hướng đối Dart. Sử dụng hệ quản cơ sở dữ liệu SQLite

10m read time

Ở bài viết này mình sẽ xây dụng ứng dụng di động quốc dân To Do List bằng Flutter và hệ quản trị cơ sở dữ liệu SQLite. Cũng như các bài viết khác mình sẽ cùng tìm hiểu sơ qua về định nghĩa của 1 ứng dụng To Do List.

ToDoList App là gì ?

ToDoList App là một ứng dụng  quản lý danh sách các công việc cần được thưc hiện. Đây là ứng dụng đầu tiên mình làm khi mình học bất cứ 1 công nghệ nào mới như Flutter, React, Vue, Node,... Bài viết này mình sẽ xây dựng ứng dụng ToDoList bằng React JS.

Demo To Do List App

Tải file APK

Một chút thông tin về Flutter :

Flutter là một SDK phát triển ứng dụng di động nguồn mở được tạo ra bởi Google. Flutter là bộ công cụ giao diện người dùng để xây dựng các ứng dụng đẹp, được biên dịch nguyên bản cho thiết bị di động, web, máy tính để bàn và thiết bị nhúng từ một cơ sở mã duy nhất ( ứng dụng đa nền tảng ).
3 điểm nổi bật nhất của Flutter :

  • Phát triển nhanhLàm cho ứng dụng của bạn trở nên sống động chỉ trong mili giây với tính năng Tải lại trạng thái nóng. Sử dụng một tập hợp phong phú các vật dụng hoàn toàn có thể tùy chỉnh để tạo giao diện gốc trong vài phút.
  • Giao diện người dùng linh hoạtNhanh chóng cung cấp các tính năng tập trung vào trải nghiệm người dùng cuối nguyên bản. Kiến trúc phân lớp cho phép tùy chỉnh đầy đủ, dẫn đến kết xuất cực kỳ nhanh chóng và các thiết kế biểu cảm và linh hoạt.
  • Hiệu suất caoCác tiện ích con của Flutter kết hợp tất cả các điểm khác biệt quan trọng của nền tảng như cuộn, điều hướng, biểu tượng và phông chữ để mang lại hiệu suất gốc đầy đủ trên cả iOS và Android.

Cơ sở dữ liệu SQLite :

SQLite là hệ thống cơ sở dữ liệu quan hệ nhỏ gọn, hoàn chỉnh, có thể cài đặt bên trong các trình ứng dụng khác. SQLite được Richard Hipp viết dưới dạng thư viện bằng ngôn ngữ lập trình C.
Ưu điểm :

  • Tin cậy: các hoạt động transaction (chuyển giao) nội trong cơ sở dữ liệu được thực hiện trọn vẹn, không gây lỗi khi xảy ra sự cố phần cứng
  • Tuân theo chuẩn SQL92 (chỉ có một vài đặc điểm không hỗ trợ)
  • Không cần cài đặt cấu hình
  • Kích thước chương trình gọn nhẹ, với cấu hình đầy đủ chỉ không đầy 300 kB
  • Thực hiện các thao tác đơn giản nhanh hơn các hệ thống cơ sở dữ liệu khách/chủ khác
  • Không cần phần mềm phụ trợ
  • Phần mềm tự do với mã nguồn mở, được chú thích rõ ràng

Cấu trúc project :

  • todolist-flutter-sqlite

    • lib

      • main.dart

      • model_todo.dart

      • db_todo.dart

      • constants.dart

    • pubspec.yaml

    • ...

Khai báo các constants :

const String toDoDone = 'TO_DO_DONE';
const String toDoProcessing = 'TO_DO_PROCESSING';

Tạo Model ToDo :

class ToDo {
	final int ? id;
	final String status;
	final String label;
	final DateTime createdAt;
	ToDo({
		this.id,
		required this.status,
		required this.label,
		required this.createdAt
	});

	ToDo copy({
			int ? id,
			String ? status,
			String ? label,
			DateTime ? createdAt,
		}) =>
		ToDo(
			id: id ?? this.id,
			status: status ?? this.status,
			label: label ?? this.label,
			createdAt: createdAt ?? this.createdAt,
		);

	Map < String, dynamic > toJson() => {
		'id': id,
		'status': status,
		'label': label,
		'createdAt': createdAt.toIso8601String()
	};

	ToDo.fromJson(Map < String, dynamic > json): id = json['id'] ?? 0,
		status = json['status'],
		label = json['label'],
		createdAt = DateTime.parse(json['createdAt'] as String);
	@override

	String toString() {
		return '"topic":'
		'{'
		'"id": $id, '
		'"status": $status, '
		'"label": $label, '
		'"createdAt": $createdAt, '
		'}';
	}
}

Thiết lập SQLite database :

import 'package:sqflite/sqflite.dart';
import 'dart:io'
as io;
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
import 'package:todolist_flutter_sqlite/model_todo.dart';

class DBToDo {
	static final DBToDo instance = DBToDo._init();
	static Database ? _db;
	DBToDo._init();
	static
	const String table = 'Topic';
	static
	const String dbName = 'db_todo.db';

	static
	const String idCol = 'id';
	static
	const String statusCol = 'status';
	static
	const String labelCol = 'label';
	static
	const String createdAtCol = 'createdAt';


	Future < Database ? > get db async {
		if (_db != null) {
			return _db;
		}
		_db = await initDb();
		return _db!;
	}

	initDb() async {
		final dbPath = await getDatabasesPath();
		String path = join(dbPath, dbName);
		var db = await openDatabase(path, version: 1, onCreate: _onCreate);
		return db;
	}

	_onCreate(Database db, int version) async {
		await db.execute("CREATE TABLE $table ($idCol INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, $statusCol TEXT NOT NULL, $labelCol TEXT NOT NULL, $createdAtCol TEXT NOT NULL)");
	}

	Future < ToDo > save(ToDo toDo) async {
		var dbClient = await instance.db;
		final id = await dbClient!.insert(table, toDo.toJson());
		return toDo.copy(id: id);
	}

	Future < List < ToDo >> getToDoList() async {
		var dbClient = await instance.db;
		List < Map < String, dynamic >> maps = await dbClient!.query(table, orderBy: "$idCol DESC", columns: [idCol, statusCol, labelCol, createdAtCol]);
		List < ToDo > todoList = [];
		for (int i = 0; i < maps.length; i++) {
			todoList.add(ToDo.fromJson(maps[i]));
		}
		return todoList;
	}

	Future < int > delete(int ? id) async {
		var dbClient = await instance.db;
		return await dbClient!.delete(table, where: '$idCol = ?', whereArgs: [id]);
	}

	Future < int > update(ToDo toDo) async {
		var dbClient = await instance.db;
		return await dbClient!.update(table, toDo.toJson(),
			where: '$idCol = ?', whereArgs: [toDo.id]);
	}

	Future < ToDo ? > show(int ? id) async {
		var dbClient = await instance.db;
		List < Map < String, dynamic >> maps = await dbClient!.query(table, where: '$idCol = ?', whereArgs: [id], columns: [idCol, statusCol, labelCol, createdAtCol]);
		if (maps.isNotEmpty) {
			return ToDo.fromJson(maps[0]);
		} else {
			return null;
		}
	}

	Future close() async {
		var dbClient = await instance.db;
		dbClient!.close();
	}
}

Khai báo state toDoList :

 List<ToDo> _toDoList = [];

dispose() được gọi khi đối tượng State bị xóa vĩnh viễn :

@override
void dispose() {
	_toDoTextController.dispose();
	DBToDo.instance.close();
	super.dispose();
}

Hàm cập nhật lại danh sách ToDoList :

Future refreshToDoList() async {
	DBToDo.instance.getToDoList().then((value) {
		setState(() {
			_toDoList = value;
		});
	});
}

Hàm thêm mới 1 ToDo :

addToDo() async {
	String label = _toDoTextController.text;
	if (label != "") {
		DateTime now = DateTime.now();
		ToDo toDo = ToDo(
			status: toDoProcessing,
			label: label,
			createdAt: now
		);
		_toDoTextController.clear();
		await DBToDo.instance.save(toDo);
		refreshToDoList();
		FocusScope.of(context).requestFocus(nodeToDoInput);
	}
}

Hàm thay đổi trạng thái 1 ToDo :

_toggleToDoStatus(ToDo toDo) async {
	ToDo toDoUpdate = toDo.copy(
		status: toDo.status == toDoDone ? toDoProcessing : toDoDone
	);
	await DBToDo.instance.update(toDoUpdate);
	refreshToDoList();
}

Hàm xóa 1 ToDo :

_deleteToDo(ToDo toDo) async {
	await DBToDo.instance.delete(toDo.id);
	refreshToDoList();
}

Hàm render toDoList :

List < Container > _listToDoContainer() {
	List < Container > listContainer = [];
	_toDoList.asMap().forEach((index, ToDo toDoItem) {
		listContainer.add(
			Container(
				padding: const EdgeInsets.symmetric(
						vertical: 5,
					),
					child: ElevatedButton(
						style: ElevatedButton.styleFrom(
							onPrimary: const Color(0xFFdddddd),
								primary: toDoItem.status == toDoDone ?
								const Color(0xFF7f7c82): index % 2 == 0 ?
									const Color(0xFFf9f9f9): const Color(0xFFeeeeee),
										padding: const EdgeInsets.symmetric(
											vertical: 0,
											horizontal: 0
										)
						),
						onPressed: () {
							_toggleToDoStatus(toDoItem);
						},
						child: Container(
							padding: const EdgeInsets.symmetric(
									vertical: 5,
								),
								child: Row(
									children: [
										Container(
											width: 40,
											child: toDoItem.status == toDoDone ?
											const Icon(
												Icons.check,
												size: 16,
											): null,
										),
										Expanded(
											child: Text(
												toDoItem.label,
												style: googleFontHandlee(
													TextStyle(
														color: toDoItem.status == toDoDone ?
														const Color(0xFFFFFFFF): const Color(0xFF000000),
															decoration: toDoItem.status == toDoDone ? TextDecoration.lineThrough : null
													)
												),
											),
										),
										GestureDetector(
											onTap: () {
												_deleteToDo(toDoItem);
											},
											child: SizedBox(
												height: 50,
												width: 50,
												child: Icon(
													Icons.close,
													size: 16,
													color: toDoItem.status == toDoDone ?
													const Color(0xFFFFFFFF): const Color(0xFF000000),
												),
											),
										)
									],
								),
						),
					),
			)
		);
	});
	return listContainer;
}

File main.dart :

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:todolist_flutter_sqlite/model_todo.dart';
import 'package:intl/intl.dart';

import 'constants.dart';
import 'db_todo.dart';



void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ToDoApp(),
    );
  }
}

class ToDoApp extends StatefulWidget {
  const ToDoApp({Key? key}) : super(key: key);

  @override
  _ToDoAppState createState() => _ToDoAppState();
}

class _ToDoAppState extends State<ToDoApp> {
  FocusNode nodeToDoInput = FocusNode();
  final TextEditingController _toDoTextController = TextEditingController(text: "");
  List<ToDo> _toDoList = [];
  @override
  void initState() {
    super.initState();
    refreshToDoList();
  }

  @override
  void dispose() {
    _toDoTextController.dispose();
    DBToDo.instance.close();
    super.dispose();
  }

  Future refreshToDoList() async {
    DBToDo.instance.getToDoList().then((value) {
      setState(() {
        _toDoList = value;
      });
    });
  }

  addToDo() async {
    String label = _toDoTextController.text;
    if (label != "") {
      DateTime now = DateTime.now();
      ToDo toDo = ToDo(
        status: toDoProcessing,
        label: label,
        createdAt: now
      );
      _toDoTextController.clear();
      await DBToDo.instance.save(toDo);
      refreshToDoList();
      FocusScope.of(context).requestFocus(nodeToDoInput);
    }
  }

  _toggleToDoStatus(ToDo toDo) async {
    ToDo toDoUpdate = toDo.copy(
      status: toDo.status == toDoDone ? toDoProcessing : toDoDone
    );
    await DBToDo.instance.update(toDoUpdate);
    refreshToDoList();
  }

  _deleteToDo(ToDo toDo) async {
    await DBToDo.instance.delete(toDo.id);
    refreshToDoList();
  }


  List<Container> _listToDoContainer() {
    List<Container> listContainer = [];
    _toDoList.asMap().forEach((index, ToDo toDoItem) {
      listContainer.add(
        Container(
            padding: const EdgeInsets.symmetric(
              vertical: 5,
            ),
            child: ElevatedButton(
              style: ElevatedButton.styleFrom(
                onPrimary: const Color(0xFFdddddd),
                primary: toDoItem.status == toDoDone ? const Color(0xFF7f7c82) : index%2 == 0 ? const Color(0xFFf9f9f9) : const Color(0xFFeeeeee),
                padding: const EdgeInsets.symmetric(
                    vertical: 0,
                    horizontal: 0
                )
              ),
              onPressed: () {
                _toggleToDoStatus(toDoItem);
              },
              child: Container(
                padding: const EdgeInsets.symmetric(
                  vertical: 5,
                ),
                child: Row(
                  children: [
                   Container(
                     width: 40,
                     child: toDoItem.status == toDoDone ? const Icon(
                       Icons.check,
                       size: 16,
                     ) : null,
                   ),
                    Expanded(
                      child: Text(
                        toDoItem.label,
                        style: googleFontHandlee(
                            TextStyle(
                            color: toDoItem.status == toDoDone ? const Color(0xFFFFFFFF) : const Color(0xFF000000),
                              decoration: toDoItem.status == toDoDone ? TextDecoration.lineThrough : null
                          )
                        ),
                      ),
                    ),
                    GestureDetector(
                      onTap: () {
                        _deleteToDo(toDoItem);
                      },
                      child: SizedBox(
                        height: 50,
                        width: 50,
                        child: Icon(
                          Icons.close,
                          size: 16,
                          color: toDoItem.status == toDoDone ? const Color(0xFFFFFFFF) : const Color(0xFF000000),
                        ),
                      ),
                    )
                  ],
                ),
              ),
            ),
          )
      );
    });
    return listContainer;
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;

    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: Container(
        height: size.height,
        width: size.width,
        decoration: const BoxDecoration(
            color: Color(0xFFf3f1f5)
        ),
        padding: const EdgeInsets.symmetric(
          vertical: 40,
          horizontal: 10
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              height: 180,
              width: size.width,
              decoration: const BoxDecoration(
                color: Color(0xFF22577a)
              ),
              padding: const EdgeInsets.symmetric(
                horizontal: 20
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    "To Do List - Flutter & SQLite",
                    style: googleFontHandlee(
                      const TextStyle(
                          color: Color(0xFFFFFFFF),
                          fontWeight: FontWeight.w500,
                          fontSize: 20
                      )
                    ),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  Row(
                    children: [
                      Expanded(
                        child: Container(
                          height: 40,
                          decoration: const BoxDecoration(
                              color: Color(0xFFFFFFFF)
                          ),
                          child: TextField(
                            autofocus: true,
                            focusNode: nodeToDoInput,
                            controller: _toDoTextController,
                            style: googleFontHandlee(
                              const TextStyle(
								  
                              )
                            ),
                            decoration: const InputDecoration(
                                hintText: "Title ...",
                                border: InputBorder.none,
                                fillColor: Color(0xFFFFFFFF),
                                filled: true
                            ),
                            onSubmitted: (value) {
                              addToDo();
                            },
                          ),
                        ),
                      ),
                      GestureDetector(
                        onTap: () {
                          addToDo();
                        },
                        child: Container(
                          width: 60,
                          height: 40,
                          alignment: Alignment.center,
                          color: const Color(0xFF9d9d9d),
                          child: Text(
                            "Add",
                            style: googleFontHandlee(
                              const TextStyle(
                                  color: Color(0xFFFFFFFF),
                                  fontWeight: FontWeight.w600
                              )
                            ),
                          ),
                        ),
                      ),
                    ],
                  )
                ],
              ),
            ),
            // body
            Container(
              height: size.height - 80 - 180,
              child: ListView(
                  shrinkWrap: true,
                  children: _listToDoContainer()
              ),
            )
          ],
        ),
      )
    );
  }
}

TextStyle googleFontHandlee(TextStyle textStyle) {
  return GoogleFonts.handlee(
    textStyle: textStyle,
  );
}

Source Code : 

https://github.com/trandinhthangdev/todolist-flutter-sqlite

APK File : 

To Do List App

2023 © Thang Tran