Flutter 使用 SQLite 本地資料庫

開發 App 時,有些資料不一定是要存在遠端資料庫,像是暫存你的登入資訊,或是單純只屬於此裝置的文件資訊,可以存放在 SQLite,這也是官方提供的方法。

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


用TodoList當作範例,資料夾結構如下:

lib/

- database/
 ⊢ TodoDB.dart
 
- pages/
 ⊢ TodoList.dart
 
- app.dart
- main.dart

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

  • 安裝package
  • 創建/連接資料庫
  • 創建table
  • 實作CRUD
  • 在頁面中引入,呼叫Function

安裝package

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

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

dependencies:
  sqflite: ^1.3.0
  path: ^1.6.4

加完之後重跑一次:

$ flutter pub get

創建/連接SQLite資料庫

因為不想直接放在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

一樣把增刪查嘅所有的function寫在同一隻檔案TodoDB.dart裡。

拿取資料 get

為了讓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,
    };
  }
}

實作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'],
      );
    });
  }
}

新增一筆 Insert

傳進來的參數,被宣告成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,
        );
    }
}

編輯 update

在設計的時候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],
        );
    }
}

刪除 delete

因為刪除只需要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],
        );
    }
}

完整原始碼請參考github

在頁面中引入,呼叫Function

引入剛剛創建的TodoDB.dart

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

查詢

在頁面中宣告:

因為我們有引入檔案,所以可以直接在widget中呼叫TodoDB.getTodos()

class _ListState extends State<ListWidget> {
  List<Todo> listArr = [];
  // 查所有list
  void getTodoList() async {
    final list = await TodoDB.getTodos();
    setState(() {
      listArr = list;
    });
  }
}

顯示:

//...
children: [
    for (int index = 1; index <= listArr.length; index++)
        ListTile(
                    leading: Checkbox(
                        value: listArr[index - 1].isCompleted == 1),
                    title: Text('${listArr[index - 1].name}'),
        )
]

新增

在頁面中宣告:

void addTodo() async {
	final newTodo = Todo(
	  id: new DateTime.now().millisecondsSinceEpoch.toString(),
	  name: 'new Todo',
	  isCompleted: 0,
	);
	await TodoDB.addTodo(newTodo);
	getTodoList();
}

按鈕呼叫

FloatingActionButton(
  onPressed: addTodo, child: const Icon(Icons.add)
)

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

在頁面中宣告:

void onChangeCheckbox(val, todo) async {
    final updateTodo =
        Todo(id: todo.id, name: todo.name, isCompleted: val ? 1 : 0);
    await TodoDB.updateTodo(updateTodo);
    getTodoList();
}

畫面呼叫

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();
  }
}

| 軟體開發 | 網站建置 | 網頁系統 | 資料庫網站 |

| 客製化網站 (報名系統、投票系統、掛號系統...) |

| 前後端技術合作 |

歡迎與我們聯繫:jessica@penueling.com

加入Line立即聊聊:@539mjyid

0
0 回復

發表評論

Want to join the discussion?
Feel free to contribute!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。