---
# System prepended metadata

title: 本地資料庫 SQLite

---

# 本地資料庫 SQLite

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

用TodoList當作範例：

![](https://i.imgur.com/TiJXJFb.png =30%x)


資料夾結構如下：
```
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();
  }
}

```