# 本地資料庫 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(); } } ```