# 本地資料庫 SQLite
依照[官網](https://flutter.dev/docs/cookbook/persistence/sqlite)的教學,是直接在`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
安裝`sqflite`跟`path`兩個package,在`pubspec.yaml`中加入:
> 兩個套件的最新版本可以上網查
```yaml
dependencies:
sqflite: ^1.3.0
path: ^1.6.4
```
加完之後重跑一次:
```
$ flutter pub get
```
## 創建/連接資料庫
因為不想直接放在`main()`裡,所以要自己創建一個`class`,可以放在獨立的資料夾。
在`lib/database/TodoDB.dart`中新增以下程式碼:
#### 引入所有會用到的package
```java
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
```
#### 創建一個schema
```java
class Todo {
final String id;
final String name;
final int isCompleted;
Todo({this.id, this.name, this.isCompleted});
}
```
#### 創建一個實例
```java
class TodoDB {
// 裡面會放所有db的動作
}
```
#### 連接資料庫
其中的`todo.db`是幫你的db取一個檔名,之後他會用這個檔名存成檔案在本地的某個資料夾中。
```java
class TodoDB {
static Database database;
static Future<Database> initDatabase() async {
database = await openDatabase(
join(await getDatabasesPath(), 'todo.db'),
);
return database;
}
}
```
為了不讓資料庫每次都要重新建立,所以加上一個防呆的判斷
```java
class TodoDB {
//...initDatabase
static Future<Database> getDBConnect() async {
if (database != null) {
return database;
}
return await initDatabase();
}
}
```
#### 創建table
`openDatabase`的第二個參數是創建table。
`execute`裡面放的是SQLite語法,`todos`是我自己取的table名稱。
```java
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`完整程式碼:
```java
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`的格式
```java
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,
};
}
}
```
2) 實作get方法:`query`裡面放的是table名稱。
```java
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的時候要帶參數進來。
```java
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`去找到唯一一筆資料修改。
```java
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`。
```java
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`
```java
import '../database/TodoDB.dart';
```
### 查詢
1) 在頁面中宣告:
因為我們有引入檔案,所以可以直接在`widget`中呼叫`TodoDB.getTodos()`
```java
class _ListState extends State<ListWidget> {
List<Todo> listArr = [];
// 查所有list
void getTodoList() async {
final list = await TodoDB.getTodos();
setState(() {
listArr = list;
});
}
}
```
2) 顯示:
```java
//...
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) 在頁面中宣告:
```java
void addTodo() async {
final newTodo = Todo(
id: new DateTime.now().millisecondsSinceEpoch.toString(),
name: 'new Todo',
isCompleted: 0,
);
await TodoDB.addTodo(newTodo);
getTodoList();
}
```
2) 按鈕呼叫
```java
FloatingActionButton(
onPressed: addTodo, child: const Icon(Icons.add))
```
### 修改(打勾完成,只修改`isCompleted`)
1) 在頁面中宣告:
```java
void onChangeCheckbox(val, todo) async {
final updateTodo =
Todo(id: todo.id, name: todo.name, isCompleted: val ? 1 : 0);
await TodoDB.updateTodo(updateTodo);
getTodoList();
}
```
2) 畫面呼叫
```java
for (int index = 1; index <= listArr.length; index++)
ListTile(
leading: Checkbox(
value: listArr[index - 1].isCompleted == 1,
onChanged: (val) {
onChangeCheckbox(val, listArr[index - 1]);
}))
```
---
## 整頁完整程式碼
```java
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();
}
}
```