最近在写 Flutter 的应用,因为 iOS 版已经上架了,但是我想的功能还没有完全做完,而且牵扯到了本地存储,于是走了很多坑。

Flutter 中的本地存储

在参考文章 1 中提到了三种本地存储的方法:

  1. Preferences 存储
  2. 文件
  3. 数据库 sqlite

因为在我的开发过程中只用到了 Prefernces 和 sqlite,所以主要围绕这两个,当然 sqlite 才是重点

Preferences 存储

写过原生安卓或者iOS应用的都知道 Shared Preferences and NSUserDefaults。在 Flutter 中也有这样的存储方式,不过需要第三方组件 shared_preferences (如何就不说了,参考文章中较为详细)

如何使用 Preferences

在我的开发过程中,设计了 Dark Mode,这里我就用 Prefernces 来存储我的 Dark Mode 状态值。

保存状态

Future<bool> save(bool isDarkMode) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  prefs.setBool("isDarkMode", isDarkMode);
}

获取状态

Future<bool> get() async {
  var isDarkMode;
  SharedPreferences prefs = await SharedPreferences.getInstance();
  isDarkMode = prefs.getBool("isDarkMode");
  if (isDarkMode == null) {
    save(false);
  }
  return isDarkMode;
}

当然为了能更好的获取 Dark Mode 切换时的体验,还是用了 redux 来做状态管理,之后也会写一篇博客来讲一下使用 redux 时遇到的坑和感受。

sqlite

同样,Flutter 本身没有 sqlite 的支持,依然需要第三方组件 sqflite

sqflite 的 Github 仓库的 README 对如何使用 sqflite 进行数据库创建 、建表、增删改查记录写的很清楚了。

// 获取本地数据库存放地址
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');

// 删除数据库
await deleteDatabase(path);

// 打开数据库
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  // 创建数据库的同时建表
  await db.execute(
      'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});

// 在一个事务中添加记录
await database.transaction((txn) async {
  int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  print('inserted1: $id1');
  int id2 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
      ['another name', 12345678, 3.1416]);
  print('inserted2: $id2');
});

// 更新记录
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, VALUE = ? WHERE name = ?',
    ['updated name', '9876', 'some name']);
print('updated: $count');

// 获取记录
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
  {'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
  {'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));

// 计算记录数量
count = Sqflite
    .firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);

// 删除记录
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// 关闭数据库
await database.close();
以上代码摘自 README 并对基本操作的注释进行了翻译

没有写在文档里的 sqflite 操作

虽然说上面这些操作已经囊括了平时开发过程中需要的数据库增删改查操作,但是今天我遇到了一个有些棘手的问题。

问题描述

比如现在我已经创建了一个 todolist 表,一个 id 一个 title 字段 CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT)

那么创建这张表的完整代码应该是这样的

Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  	await db.execute(
      	'CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT)');
});

但是这时候我想添加一个 分类 功能,该怎么办呢?虽然我对移动端的 sqlite 了解并不多,但从我无数次的尝试中可以发现, onCreate 似乎只会执行一次,所以如果你强行更改 onCreate 中的 sql 语句是不会起作用的,除非卸载 App 重新安装,很显然不能这样做,你不能让用户卸载了再安装吧。

于是我在想有没有什么办法手动进行更新,比如设置一个版本号变量,如果用户存储的版本号比当前低,那么进行数据库更新操作,并将存储的版本号更新。

但是这个方法太烦(不过后来想想这个方法是可行的,因为它的原理和下面的方法是类似的)

于是我竟然去看了 sqflite 的源码?!发现 openDatabase 的参数中不仅仅有 onCreate 还有一个叫做 onUpgrade 的参数

[onUpgrade] is called if either of the following conditions are met:

  • [onCreate] is not specified
  • The database already exists and [version] is higher than the last database version

In the first case where [onCreate] is not specified, [onUpgrade] is called with its [oldVersion] parameter as 0. In the second case, you can perform the necessary migration procedures to handle the differing schema

这里的第二条意思大概就是如果当前的数据库版本比最后的高,那么执行 onUpgrade 中的操作,是不是和我之前那个想法如出一辙。于是数据库操作的更新就能在 onUpgrade 中写。

小结

关于我遇到的这个问题资料很少,而且不太容易表达清楚,而很多教程只是教你基本的增删改查操作,这些看官方文档足矣,所以有时候看源码也是一个不错的解决方案,说不定就能从中找到解决问题的关键。

参考文章

  1. [Flutter中的本地存储](http://flutter.link/2018/04/13/Flutter中的本地存储/)
  2. [/sqflite: SQLite flutter plugin](https://github.com/tekartik/sqflite)