Flutter 使用 SQLite 本地資料庫

Jess
再不寫就要忘了
17 min readAug 13, 2020

--

依照官網的教學,是直接在main()裡面進行所有操作,但是為了看起來更好閱讀更簡潔,建議新增一個單獨的資料夾database/,裡面存放要建立的${dbName}.dart檔案。

用TodoList當作範例:

資料夾結構如下:

lib/

- database/
⊢ TodoDB.dart

- pages/
⊢ TodoList.dart

- app.dart
- main.dart

要建立一個db,大概會經過以下步驟:

  1. 安裝package
  2. 創建/連接資料庫
  3. 創建table
  4. 實作CRUD
  5. 在頁面中引入,呼叫Function

安裝package

安裝sqflitepath兩個package,在pubspec.yaml中加入:

兩個套件的最新版本可以上網查

dependencies:
sqflite: ^1.3.0
path: ^1.6.4

加完之後重跑一次:

$ flutter pub get

創建/連接資料庫

因為不想直接放在main()裡,所以要自己創建一個class,可以放在獨立的資料夾。
lib/database/TodoDB.dart中新增以下程式碼:

引入所有會用到的package

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

創建一個schema

class Todo {
final String id;
final String name;
final int isCompleted;
Todo({this.id, this.name, this.isCompleted});
}

創建一個實例

class TodoDB {
// 裡面會放所有db的動作
}

連接資料庫

其中的todo.db是幫你的db取一個檔名,之後他會用這個檔名存成檔案在本地的某個資料夾中。

class TodoDB {
static Database database;

static Future<Database> initDatabase() async {
database = await openDatabase(
join(await getDatabasesPath(), 'todo.db'),
);
return database;
}
}

為了不讓資料庫每次都要重新建立,所以加上一個防呆的判斷

class TodoDB {    //...initDatabase

static Future<Database> getDBConnect() async {
if (database != null) {
return database;
}
return await initDatabase();
}
}

創建table

openDatabase的第二個參數是創建table。
execute裡面放的是SQLite語法,todos是我自己取的table名稱。

static Future<Database> initDatabase() async {
database = await openDatabase(
join(await getDatabasesPath(), 'todo.db'),
onCreate: (db, version) {
return db.execute(
"CREATE TABLE todos(id TEXT PRIMARY KEY, name TEXT, isCompleted INTEGER)",
);
},
version: 1,
);
return database;
}

到目前為止的TodoDB.dart完整程式碼:

import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
class Todo {
final String id;
final String name;
final int isCompleted;
Todo({this.id, this.name, this.isCompleted});
}
class TodoDB {
static Database database;
static Future<Database> getDBConnect() async {
if (database != null) {
return database;
}
return await initDatabase();
}
static Future<Database> initDatabase() async {
database = await openDatabase(
join(await getDatabasesPath(), 'todo.db'),
onCreate: (db, version) {
return db.execute(
"CREATE TABLE todos(id TEXT PRIMARY KEY, name TEXT, isCompleted INTEGER)",
);
},
version: 1,
);
return database;
}
}

實作CRUD

拿取資料

  1. 為了讓schema可以產出直接可以使用的資料,所以先修改Todo讓它return出Map的格式
class Todo {
final String id;
final String name;
final int isCompleted;
Todo({this.id, this.name, this.isCompleted});
// 加上下面這些...
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'isCompleted': isCompleted,
};
}
}
  1. 實作get方法:query裡面放的是table名稱。
class TodoDB {
//...initDatabase


static Future<List<Todo>> getTodos() async {
final Database db = await getDBConnect();
final List<Map<String, dynamic>> maps = await db.query('todos');
return List.generate(maps.length, (i) {
return Todo(
id: maps[i]['id'],
name: maps[i]['name'],
isCompleted: maps[i]['isCompleted'],
);
});
}
}

新增一筆

傳進來的參數,被宣告成Todo的型態,參數名稱叫做todo,到時候頁面中呼叫這個function的時候要帶參數進來。

class TodoDB {
//...initDatabase

static Future<void> addTodo(Todo todo) async {
final Database db = await getDBConnect();
await db.insert(
'todos',
todo.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}

編輯

在設計的時候id規定是唯一不重複的值,所以可以用id去找到唯一一筆資料修改。

class TodoDB {
//...initDatabase

static Future<void> updateTodo(Todo todo) async {
final Database db = await getDBConnect();
await db.update(
'todos',
todo.toMap(),
where: "id = ?",
whereArgs: [todo.id],
);
}
}

刪除

因為刪除只需要id去找到該筆資料然後刪掉,所以呼叫的時候,只需要傳id進來,不需要給整個Todo

class TodoDB{
static Future<void> deleteTodo(String id) async {
final Database db = await getDBConnect();
await db.delete(
'todos',
where: "id = ?",
whereArgs: [id],
);
}
}

在頁面中引入,呼叫Function

lib/pages/TodoList.dart :

引入剛剛創建的TodoDB.dart

import '../database/TodoDB.dart';

查詢

  1. 在頁面中宣告:
    因為我們有引入檔案,所以可以直接在widget中呼叫TodoDB.getTodos()
class _ListState extends State<ListWidget> {
List<Todo> listArr = [];
// 查所有list
void getTodoList() async {
final list = await TodoDB.getTodos();
setState(() {
listArr = list;
});
}
}
  1. 顯示:
//...children: [
for (int index = 1; index <= listArr.length; index++)
ListTile(
leading: Checkbox(
value: listArr[index - 1].isCompleted == 1),
title: Text('${listArr[index - 1].name}'),
)
]

新增

  1. 在頁面中宣告:
void addTodo() async {
final newTodo = Todo(
id: new DateTime.now().millisecondsSinceEpoch.toString(),
name: 'new Todo',
isCompleted: 0,
);
await TodoDB.addTodo(newTodo);
getTodoList();
}
  1. 按鈕呼叫
FloatingActionButton(
onPressed: addTodo, child: const Icon(Icons.add))

修改(打勾完成,只修改isCompleted

  1. 在頁面中宣告:
void onChangeCheckbox(val, todo) async {
final updateTodo =
Todo(id: todo.id, name: todo.name, isCompleted: val ? 1 : 0);
await TodoDB.updateTodo(updateTodo);
getTodoList();
}
  1. 畫面呼叫
for (int index = 1; index <= listArr.length; index++)
ListTile(
leading: Checkbox(
value: listArr[index - 1].isCompleted == 1,
onChanged: (val) {
onChangeCheckbox(val, listArr[index - 1]);
}))

整頁完整程式碼

import 'package:flutter/material.dart';
import '../database/TodoDB.dart';
enum extraAction { edit, delete }class ListWidget extends StatefulWidget {
@override
_ListState createState() => _ListState();
}
class _ListState extends State<ListWidget> {
List<Todo> listArr = [];
// 查所有list
void getTodoList() async {
final list = await TodoDB.getTodos();
setState(() {
listArr = list;
});
}
// 打勾
void onChangeCheckbox(val, todo) async {
final updateTodo =
Todo(id: todo.id, name: todo.name, isCompleted: val ? 1 : 0);
await TodoDB.updateTodo(updateTodo);
getTodoList();
}
// 新增
void addTodo() async {
final newTodo = Todo(
id: new DateTime.now().millisecondsSinceEpoch.toString(),
name: 'new Todo',
isCompleted: 0,
);
await TodoDB.addTodo(newTodo);
getTodoList();
}
// 選擇編輯 or 刪除
void editList(type, context, todo) {
switch (type) {
case extraAction.edit:
Navigator.push<void>(
context,
MaterialPageRoute(
builder: (context) => FullScreenDialog(
onSave: editTodo, todo: todo, onDelete: deleteTodo),
fullscreenDialog: true));
break;
case extraAction.delete:
deleteTodo(todo);
break;
default:
print('error!!');
}
}
// 編輯
void editTodo(name, todo) async {
final updateTodo =
Todo(id: todo.id, name: name, isCompleted: todo.isCompleted);
await TodoDB.updateTodo(updateTodo);
getTodoList();
Navigator.pop(context);
}
// 刪除
void deleteTodo(todo) async {
await TodoDB.deleteTodo(todo.id);
getTodoList();
}
@override
void initState() {
super.initState();
getTodoList();
}
void dispose() {
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TodoList'),
),
body: Column(
children: <Widget>[
Expanded(
child: ListView(
children: [
for (int index = 1; index <= listArr.length; index++)
ListTile(
leading: Checkbox(
value: listArr[index - 1].isCompleted == 1,
onChanged: (val) {
onChangeCheckbox(val, listArr[index - 1]);
}),
title: Text('${listArr[index - 1].name}',
style: TextStyle(
color: listArr[index - 1].isCompleted == 1
? Colors.grey.shade400
: Theme.of(context).textTheme.bodyText1.color,
decoration: listArr[index - 1].isCompleted == 1
? TextDecoration.lineThrough
: null)),
trailing: PopupMenuButton<extraAction>(
onSelected: (type) {
editList(type, context, listArr[index - 1]);
},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<extraAction>>[
PopupMenuItem<extraAction>(
value: extraAction.edit,
child: Text('Edit'),
),
PopupMenuItem<extraAction>(
value: extraAction.delete,
child: Text('Delete'),
),
],
),
)
],
),
),
MaterialBanner(
content: Text(
'共 ${listArr.length} 個清單,已完成 ${listArr.where((obj) => obj.isCompleted == 1).length} 個'),
actions: <Widget>[null],
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: addTodo, child: const Icon(Icons.add)),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
}
class FullScreenDialog extends StatefulWidget {
FullScreenDialog({this.onSave, this.todo, this.onDelete});
final Function onSave;
final Todo todo;
final Function onDelete;
@override
_FullScreenDialogState createState() => _FullScreenDialogState();
}
class _FullScreenDialogState extends State<FullScreenDialog> {
final itemController = TextEditingController();
@override
void initState() {
super.initState();
itemController.text = widget.todo.name;
}
void dispose() {
itemController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('add a todo'),
actions: <Widget>[
FlatButton(
onPressed: () {
widget.onSave(itemController.text, widget.todo);
},
child: Text('儲存'))
],
),
body: Center(
child: Container(
padding: const EdgeInsets.all(30.0),
child: TextField(
controller: itemController,
obscureText: false,
decoration:
InputDecoration(labelText: 'label', hintText: '請輸入項目名稱'),
),
),
),
);
}
}
class TodoList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListWidget();
}
}

--

--

Jess
再不寫就要忘了

之後的文章都會搬到https://penueling.com發表囉!