Flutter 是 Google 開(kāi)源的 UI 工具包,一套代碼多端應(yīng)用極大的提升了開(kāi)發(fā)效率,此外直接調(diào)用skia(c/c++)代碼的能力,也使得它具備媲美原生的渲染性能。但是涉及到非UI層的任務(wù)時(shí),F(xiàn)lutter仍然需要依托原生框架,比如相機(jī)、存儲(chǔ)、藍(lán)牙等功能,于是各種對(duì)原生能力封裝的Plugin就產(chǎn)生了,Android、iOS各自平臺(tái)提供原生能力,flutter側(cè)進(jìn)行對(duì)接,提供dart語(yǔ)言編寫(xiě)的api 給flutter側(cè)調(diào)用,使得flutter開(kāi)發(fā)人員依然可以一套代碼,多端應(yīng)用。
存儲(chǔ)數(shù)據(jù)到磁盤是開(kāi)發(fā)中常見(jiàn)的操作,比如用戶信息、一些不經(jīng)常變動(dòng)的數(shù)據(jù)、通訊錄等,以便下次打開(kāi)APP用戶可以不經(jīng)過(guò)網(wǎng)絡(luò)請(qǐng)求,快速預(yù)覽APP中的內(nèi)容。根據(jù)需要存儲(chǔ)的數(shù)據(jù)量的大小,用戶可以選擇適合自己的方案。對(duì)于少量的數(shù)據(jù),在原生側(cè)iOS一般直接使用UserDefaults,Android使用SharedPreferences,這兩種存儲(chǔ)方式一般用來(lái)存儲(chǔ)用戶或者APP信息等少量的數(shù)據(jù)。當(dāng)數(shù)據(jù)量大的時(shí)候就不適合使用了,一般會(huì)考慮基于SQLite的數(shù)據(jù)庫(kù)存儲(chǔ),或者是基于文件的存儲(chǔ)。
以上也是本文將要要講述核心:詳細(xì)介紹幾個(gè)存儲(chǔ)的優(yōu)質(zhì)框架的原理及使用,以便對(duì)大家需要使用存儲(chǔ)能力的時(shí)候有所幫助。
少量數(shù)據(jù)存儲(chǔ)
少量數(shù)據(jù)建議直接使用shared_preferences
這是官方維護(hù)的倉(cāng)庫(kù),它是對(duì)iOS中UserDefaults和Android中SharedPreferences的plugin封裝,iOS UserDefaults存儲(chǔ)在plist中,Android preferences存儲(chǔ)在xml中,原本各自操作都很簡(jiǎn)單,所以flutter側(cè)的封裝也很簡(jiǎn)單,整個(gè)代碼包含注釋不到200行。
因此在flutter側(cè)基于它來(lái)進(jìn)行少量數(shù)據(jù)的存儲(chǔ)也是十分方便的,在flutter側(cè)的類名也叫SharedPreferences
,是個(gè)單例,實(shí)例化的時(shí)候會(huì)從磁盤中讀取到內(nèi)存,并且在內(nèi)存中保存一份,之后如果有新的數(shù)據(jù)存入的話,會(huì)同時(shí)進(jìn)行內(nèi)存和磁盤的更新,當(dāng)然寫(xiě)磁盤操作有極小的概率可能失敗,因此內(nèi)存中數(shù)據(jù)和磁盤中數(shù)據(jù)有極小概率不一致。
Future<bool> _setValue(String valueType, String key, Object value) {
final String prefixedKey = '$_prefix$key';
if (value == null) {
_preferenceCache.remove(key);
return _store.remove(prefixedKey);
} else {
if (value is List<String>) {
// Make a copy of the list so that later mutations won't propagate
_preferenceCache[key] = value.toList();
} else {
_preferenceCache[key] = value;
}
return _store.setValue(valueType, prefixedKey, value);
}
}
還需要注意點(diǎn)的一點(diǎn)是,如果native側(cè)進(jìn)行了SharedPreferences或者NSUserDefaults的存儲(chǔ)、修改操作,flutter側(cè)的SharedPreferences
單例,并不會(huì)自行更新到內(nèi)存中,需要調(diào)用reload
方法進(jìn)行內(nèi)存的更新。
Future<void> reload() async {
final Map<String, Object> preferences =
await SharedPreferences._getSharedPreferencesMap();
_preferenceCache.clear();
_preferenceCache.addAll(preferences);
}
// 讀取操作api
Set<String> getKeys()
dynamic get(String key)
bool getBool(String key)
int getInt(String key)
double getDouble(String key)
String getString(String key)
bool containsKey(String key)
List<String> getStringList(String key)
// 寫(xiě)入操作api
Future<bool> setBool(String key, bool value)
Future<bool> setInt(String key, int value)
Future<bool> setDouble(String key, double value)
Future<bool> setString(String key, String value)
Future<bool> setStringList(String key, List<String> value)
Future<bool> remove(String key)
在項(xiàng)目開(kāi)發(fā)的時(shí)候,我們先獲取preferences單例,然后按照上述api進(jìn)行操作即可,由于比較簡(jiǎn)單,這里不再做實(shí)際示例介紹。
大量數(shù)據(jù)存儲(chǔ)
數(shù)據(jù)量大的話一般會(huì)基予SQLite進(jìn)行操作,目前flutter側(cè)最好的基于sqlite的插件是sqflite
它在原生iOS側(cè)基于FMDB封裝,Android側(cè)基于系統(tǒng)的sqlite封裝,使用起來(lái)比preferences稍微復(fù)雜點(diǎn),需要打開(kāi)關(guān)閉數(shù)據(jù)庫(kù),自己建表進(jìn)行增、刪、改、查。其原理通過(guò)channel通信,將sql指令發(fā)送到原生側(cè),原生操作完數(shù)據(jù)庫(kù),再將數(shù)據(jù)返回給flutter側(cè)。plugin flutter側(cè)將用戶的操作最終都是轉(zhuǎn)化為sql指令,當(dāng)然flutter側(cè)不只是轉(zhuǎn)發(fā)用戶的sql操作,接下來(lái)會(huì)詳細(xì)進(jìn)行講述。
使用sqlite存儲(chǔ) demo地址
- 首先是獲取默認(rèn)數(shù)據(jù)庫(kù)存放路徑
var databasesPath = await getDatabasesPath();
通過(guò)getDatabasesPath
第一次調(diào)用的時(shí)候,通過(guò)channel向native側(cè)發(fā)送一條消息,native將路徑地址返回給flutter側(cè),flutter緩存此地址,之后再調(diào)用此方法,直接返回緩存的地址。
- 然后定義自己的數(shù)據(jù)庫(kù)db路徑
String path = join(databasesPath, 'demo.db');
以上兩步操作的結(jié)果:
flutter: databasesPath /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents
flutter: db path /var/mobile/Containers/Data/Application/9EAD6644-1A9A-4741-BC5E-51D9D678CA30/Documents/demo.db
這時(shí)只是獲取了地址db實(shí)例并不會(huì)創(chuàng)建。
創(chuàng)建數(shù)據(jù)庫(kù)
Database database = await openDatabase(_dbPath);
創(chuàng)建數(shù)據(jù)庫(kù)最少只需要給定一個(gè)路徑就行,當(dāng)然這個(gè)api中還有其他可選參數(shù)
Future<Database> openDatabase(String path,
{int version,
OnDatabaseConfigureFn onConfigure,
OnDatabaseCreateFn onCreate,
OnDatabaseVersionChangeFn onUpgrade,
OnDatabaseVersionChangeFn onDowngrade,
OnDatabaseOpenFn onOpen,
bool readOnly = false,
bool singleInstance = true})
- version db版本,用來(lái)決定是否進(jìn)行創(chuàng)建、升降級(jí)。只有設(shè)置了version,onCreate、onUpgrade、onDowngrade這三個(gè)可選回調(diào)才可能被調(diào)用,這三個(gè)回調(diào)最多只會(huì)調(diào)用一個(gè),當(dāng)version不變時(shí),三個(gè)回調(diào)都不會(huì)被調(diào)用。
- onConfigure 打開(kāi)db時(shí)首先執(zhí)行這個(gè)回調(diào),在這個(gè)回調(diào)中可以執(zhí)行db的初始化操作,比如外鍵的設(shè)置或者提前寫(xiě)日志。
- onCreate 只有當(dāng)db不存在時(shí),第一次調(diào)用openDatabase才會(huì)執(zhí)行此回調(diào),可以利用這個(gè)時(shí)機(jī),創(chuàng)建一些所需的table。
- onUpgrade 有兩個(gè)場(chǎng)景會(huì)執(zhí)行此回調(diào),1.初始創(chuàng)建db時(shí),onCreate回調(diào)未設(shè)置。2.db已經(jīng)存在,并且version比db中上次記錄的的version大。可以在此方法中執(zhí)行必要的遷移操作。
- onDowngrade 只有version比db中記錄的的version小時(shí)才會(huì)執(zhí)行。這種情況很少見(jiàn),只有當(dāng)新版本的代碼創(chuàng)建了一個(gè)數(shù)據(jù)庫(kù),然后與舊版本的代碼交互時(shí)才會(huì)出現(xiàn)這種情況,應(yīng)該盡量避免這種情況。
- onOpen 這個(gè)回調(diào)最后執(zhí)行,在version被重置之后,openDatabase返回結(jié)果之前。
- readOnly 默認(rèn)false,如果設(shè)置為true,則不允許任何修改操作
- singleInstance 默認(rèn)為true,這樣針對(duì)同樣的dbPath,將返回同一個(gè)db實(shí)例。當(dāng)多次調(diào)用openDatabase的時(shí)候,只有首次調(diào)用的回調(diào)會(huì)生效,再次調(diào)用同一path時(shí)候,只會(huì)返回db實(shí)例,忽略新設(shè)置的參數(shù)。
這幾個(gè)可選回調(diào)順序是
1. [onConfigure]
2. [onCreate] or [onUpgrade] or [onDowngrade]
5. [onOpen]
我們可以創(chuàng)建多個(gè)db實(shí)例
,每個(gè)db實(shí)例中可以創(chuàng)建多張table
。這點(diǎn)和原生操作數(shù)據(jù)庫(kù)是一致的。
除了在每次數(shù)據(jù)庫(kù)初始創(chuàng)建onCreate或者升級(jí)時(shí)的onUpgrade回調(diào)中操作表,還可以在其他時(shí)機(jī)進(jìn)行操作,我們可以新建表、修改表字段,修改表字段對(duì)應(yīng)值得類型等,對(duì)表的操作都是通過(guò)sql語(yǔ)句進(jìn)行操作,下面展示幾個(gè)示例:
- 新增一張表
_database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)');
如果db中已經(jīng)存在相同的表,再次創(chuàng)建不會(huì)生效,對(duì)原有表不會(huì)有影響。
- 新增表字段
_database.execute('alter table Test2 ADD num2 REAL NOT NULL Default 0');
- 更改表字段
_database.execute('alter table Test2 rename column num to num3');
合適的場(chǎng)景是,當(dāng)表里的字段不滿足時(shí),可以在數(shù)據(jù)庫(kù)升級(jí)的回調(diào)onUpgrade中進(jìn)行表的更改,當(dāng)然這有業(yè)務(wù)決定。
其他操作,還有這些sql語(yǔ)句和原生側(cè)操作一樣。
修改字段默認(rèn)值
alter table 表名 drop constraint 約束名字 // 刪除表的字段的原有約束
alter table 表名 add constraint 約束名字 DEFAULT 默認(rèn)值 for 字段名稱 // 添加一個(gè)表的字段的約束并指定默認(rèn)值
修改字段類型:
alter table 表名 alter column name nvarchar(10) not null
當(dāng)數(shù)據(jù)庫(kù)和表都建立好之后,接下來(lái)就是數(shù)據(jù)操作了,這里flutter側(cè)有兩種方式操作,可以直接編寫(xiě)sql語(yǔ)句,也可以使用flutter側(cè) helpers操作,兩種操作各有優(yōu)劣。Raw Sql方式更加直觀,但是sql語(yǔ)句編寫(xiě)容易出錯(cuò)
-- | Raw Sql | SQL helpers |
---|---|---|
優(yōu)勢(shì) | 直觀,sql直接發(fā)送到native側(cè)處理 | 書(shū)寫(xiě)簡(jiǎn)單,不易出錯(cuò) |
劣勢(shì) | 直接編寫(xiě)sql 語(yǔ)句容易出錯(cuò) | 需要一層轉(zhuǎn)換,底層仍是調(diào)用Raw Sql方式 |
- Raw Sql方式
// 增
int id1 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
// 刪
await _database.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
// 改
await _database.rawUpdate('UPDATE Test SET name = ?, value = ? WHERE name = ?', ['updated name', '9876', 'some name']);
// 查
List<Map> list = await _database.rawQuery('SELECT * FROM Test');
- SQL helpers
SQL helpers是flutter側(cè)對(duì)直接操作sql的封裝,以insert
為例,借助SqlBuilder
這個(gè)類提供的能力,將SQL helpers Api轉(zhuǎn)化成sql字符串,最終還是轉(zhuǎn)換為RAW Sql方式執(zhí)行。
Future<int> insert(String table, Map<String, dynamic> values,
{String nullColumnHack, ConflictAlgorithm conflictAlgorithm}) {
final builder = SqlBuilder.insert(table, values,
nullColumnHack: nullColumnHack, conflictAlgorithm: conflictAlgorithm);
return rawInsert(builder.sql, builder.arguments);
}
// 增
// table表名;values Map數(shù)據(jù),可以是model2json轉(zhuǎn)成的數(shù)據(jù);
// nullColumnHack字段為空時(shí)處理語(yǔ)句,conflictAlgorithm沖突處理枚舉
uture<int> insert(String table, Map<String, dynamic> values,
{String nullColumnHack, ConflictAlgorithm conflictAlgorithm});
// 刪
// where篩選條件,如果where為null,則刪除整個(gè)表中的數(shù)據(jù),whereArgs即其參數(shù)
Future<int> delete(String table, {String where, List<dynamic> whereArgs});
// 改
// values將要更新到表中的值,如果后面的篩選條件不設(shè)置,將更新整個(gè)表
Future<int> update(String table, Map<String, dynamic> values,
{String where,
List<dynamic> whereArgs,
ConflictAlgorithm conflictAlgorithm});
// 查
// distinct是否排重,true的話返回的每行數(shù)據(jù)都是唯一的
// 返回表中哪幾列的數(shù)據(jù),傳null將返回所有列,最好對(duì)數(shù)據(jù)進(jìn)行過(guò)濾,以免讀取太多不相干的數(shù)據(jù)
Future<List<Map<String, dynamic>>> query(String table,
{bool distinct,
List<String> columns,
String where,
List<dynamic> whereArgs,
String groupBy,
String having,
String orderBy,
int limit,
int offset});
SQL helpers可以和數(shù)據(jù)模型結(jié)合使用,根據(jù)業(yè)務(wù)需求,編寫(xiě)對(duì)應(yīng)的增刪改查api,外部使用就會(huì)非常精簡(jiǎn),下面是個(gè)具體的小例子:
final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';
class Todo {
int id;
String title;
bool done;
Map<String, dynamic> toMap() {
var map = <String, dynamic>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}
Todo();
Todo.fromMap(Map<String, dynamic> map) {
id = map[columnId];
title = map[columnTitle];
done = map[columnDone] == 1;
}
}
class TodoProvider {
Database db;
Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}
Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}
Future<Todo> getTodo(int id) async {
List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Todo.fromMap(maps.first);
}
return null;
}
Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}
Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id]);
}
Future close() async => db.close();
}
以上代碼可以直在sqlite存儲(chǔ) demo地址中查看。
- 事務(wù)
如果有一組不可分割的操作,可以使用transaction進(jìn)行處理,也即事務(wù)
Future<T> transaction<T>(Future<T> Function(Transaction txn) action,
{bool exclusive});
// 一個(gè)具體小例子
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');
});
需要注意的是,事務(wù)中不要直接再調(diào)用database進(jìn)行操作,否則可能造成死鎖,而是使用變量txn操作。
- 批處理
經(jīng)過(guò)上面的介紹我們可以知道,所有操作都是通過(guò)channel發(fā)送到原生側(cè)處理的,操作多的話,就會(huì)產(chǎn)生很多的來(lái)回通信數(shù)據(jù),為了優(yōu)化這個(gè)過(guò)程可以將一組操作放到Batch中,它會(huì)將一組指令打包成一條,注意最后有個(gè)batch.commit();
操作
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
results = await batch.commit();
存儲(chǔ)數(shù)據(jù)到文件
除了以上兩種方式,還可以將數(shù)據(jù)存儲(chǔ)到文件中,這里也推薦官方維護(hù)的path_provider
- 首先 仍是需要制定/獲取文件路徑
Future<String> get _localPath async {
Directory _path = await getApplicationDocumentsDirectory();
Directory _directory = await Directory("${_path.path}/test").create(recursive: true);
return _directory.path;
}
- 然后獲取文件
文件格式類型按自己需求指定。
Future<File> get _localFile async {
final path = await _localPath;
return File('$path/homePageId001.json');
}
- 讀
讀寫(xiě)操作不只有字符串,還可以是其他類型。
Future<String> readStr() async {
try {
final file = await _localFile;
var content = await file.readAsString();
return content;
} catch (e) {
return 'error';
}
}
- 寫(xiě)
Future<File> writeStr(str) async {
final file = await _localFile;
return file.writeAsString('$str');
}
開(kāi)發(fā)時(shí)檢查
無(wú)論是以上哪種方式,都是可以在開(kāi)發(fā)時(shí)查看存儲(chǔ)到磁盤中的數(shù)據(jù)是否正常,這也和原生開(kāi)發(fā)時(shí)類似。以iOS為例,每個(gè)APP運(yùn)行數(shù)據(jù)都是隔離的,有自己的文件夾,我們可以將所開(kāi)發(fā)APP的文件夾下載下來(lái),選中Xcode-> Window -> Devices and simulators 然后按照下圖操作:
選中運(yùn)行的程序,下載所在APP運(yùn)行的數(shù)據(jù),雙擊顯示包內(nèi)容:
我們的db文件
和操作文件
存儲(chǔ)在此,preferences
文件存儲(chǔ)在AppData -> Library -> Preferences目錄下,操作文件
和preferences
可以直接打開(kāi)查看,這里說(shuō)下db文件:
每次對(duì)數(shù)據(jù)有修改,我們可以在開(kāi)發(fā)時(shí),查看對(duì)應(yīng)db中的表或者數(shù)據(jù)的改動(dòng)。
以上就是我對(duì)Flutter中存儲(chǔ)的總結(jié),大家根據(jù)業(yè)務(wù)情況選擇適合自己的方案,當(dāng)然還有其他優(yōu)秀框架,歡迎一起交流。