該文已授權(quán)公眾號 「碼個蛋」卦停,轉(zhuǎn)載請指明出處
上節(jié)講了狀態(tài)管理,但是當(dāng) App
重啟后恼蓬,數(shù)據(jù)就都丟失了惊完,這樣就比較尷尬了,什么都要重來处硬,所以這節(jié)我們來講下數(shù)據(jù)持久化小槐。數(shù)據(jù)持久化主要有如下方式
- 文件讀寫
-
shared_preferences
存儲 - 數(shù)據(jù)庫存儲
持久化的實現(xiàn)都需要通過三方插件來實現(xiàn),接著會慢慢介紹三種實現(xiàn)方式
文件讀寫/ IO 操作
文件讀寫需要 path_provider
插件郁油,寫這篇文章的時候本股,最新版本是 0.5.0+1
攀痊,小伙伴們可以根據(jù)官網(wǎng)最新的版本進行替換桐腌,導(dǎo)入后我們就可以來看下如何實現(xiàn)文件的讀寫了拄显。path_provider
的源碼比較簡單,這邊就不單獨拎出來說了案站,可以自行查看躬审。path_provider
用于獲取手機的存儲文件位置,一共有三個方法
-
getTemporaryDirectory
臨時目錄蟆盐,在 Android 中對應(yīng)的方法為getCacheDir
承边,而在 iOS 中對應(yīng)為NSCachesDirectory
,可以通過系統(tǒng)檢測并清除 -
getApplicationDocumentsDirectory
緩存目錄石挂,在 Android 中對應(yīng)為AppData
文件夾博助,在 iOS 中對應(yīng)為NSDocumentsDirectory
,只有當(dāng) App 被刪除才能被刪除 -
getExternalStorageDirectory
外部存儲目錄痹愚,只有在 Android 中有效富岳,在 iOS 調(diào)用會拋出UnsupportedError
異常,不過 Android 在寫入前記得先申請權(quán)限喲拯腮,否則也是不行滴窖式。
讀寫文件操作需要通過 Dart
的 IO
操作完成,這邊小伙伴們可以自己看文檔 File class动壤,接著我們就直接通過例子來看文件實現(xiàn)數(shù)據(jù)持久化萝喘。先看下效果吧,最終重啟 App 后琼懊,數(shù)據(jù)也能正常讀取顯示阁簸,說明數(shù)據(jù)被保存下來了
看下實現(xiàn)的代碼,因為會涉及到多種方式哼丈,所以這邊我把視圖抽取出來實現(xiàn)
Widget _fileIoPart() {
return Card(
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
child: Column(children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Text('File IO', style: TextStyle(fontSize: 20.0, color: Theme.of(context).primaryColor)),
),
// RadioList 是單選按鈕部件启妹,通過選擇不同的情況,創(chuàng)建不同目錄的文件
RadioListTile(
value: _radioText[0],
title: Text(_radioText[0]),
subtitle: Text(_radioDescriptions[0]),
groupValue: _currentValue,
onChanged: ((value) {
setState(() => _currentValue = value);
})),
RadioListTile(
value: _radioText[1],
title: Text(_radioText[1]),
subtitle: Text(_radioDescriptions[1]),
groupValue: _currentValue,
onChanged: ((value) {
setState(() => _currentValue = value);
})),
RadioListTile(
value: _radioText[2],
title: Text(_radioText[2]),
subtitle: Text(_radioDescriptions[2]),
groupValue: _currentValue,
onChanged: ((value) {
setState(() => _currentValue = value);
})),
Padding(
padding: const EdgeInsets.all(12.0),
// 用于寫入文本信息
child: TextField(
controller: _editController,
decoration: InputDecoration(labelText: '輸入存儲的文本內(nèi)容', icon: Icon(Icons.text_fields)),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0),
width: MediaQuery.of(context).size.width,
child: RaisedButton(
onPressed: _writeTextIntoFile,
child: Text('寫入文件信息'),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Text('文件內(nèi)容:'), Expanded(child: Text(_fileContent, softWrap: true))],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0),
width: MediaQuery.of(context).size.width,
child: RaisedButton(
onPressed: _readTextFromFile,
child: Text('讀取文件信息'),
),
),
]),
);
}
關(guān)鍵的部分在于 _writeTextIntoFile
和 _readTextFromFile
兩個方法的實現(xiàn)削祈〕崮纾看下實現(xiàn)的代碼
// 如果寫入外部內(nèi)存需要讀寫權(quán)限,這邊使用了第三方插件 `permission_handler`
void _writeTextIntoFile() async {
if (_currentValue == _radioText[2]) {
PermissionStatus status = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage);
if (status == PermissionStatus.granted) // 如果是寫入外部存儲髓抑,則檢測權(quán)限狀態(tài)咙崎,同意則寫入
_writeContent();
else if (status == PermissionStatus.disabled) // 拒絕了提示手動打開
Fluttertoast.showToast(msg: '未打開相關(guān)權(quán)限');
else // 未同意則主動申請權(quán)限
PermissionHandler().requestPermissions([PermissionGroup.storage]);
} else // 不是寫入外部存儲直接寫入文件
_writeContent();
}
// 文本寫入文件
void _writeContent() async {
// 寫入文本操作
var text = _editController.value.text; // 獲取文本框的內(nèi)容
File file = File(await _getFilePath()); // 獲取相應(yīng)的文件
if (text == null || text.isEmpty) {
Fluttertoast.showToast(msg: '請輸入內(nèi)容'); // 內(nèi)容為空,則不寫入并提醒
} else {
// 內(nèi)容不空吨拍,則判斷是否已經(jīng)存在褪猛,存在先刪除,重新創(chuàng)建后寫入信息
if (await file.exists()) file.deleteSync();
file.createSync(); // createSync 是一個同步的創(chuàng)建過程
file.writeAsStringSync(text); // writeAsStringSync 是同步寫入的過程
_editController.clear(); // 寫入文件后清空輸入框信息
}
}
// 讀取文本操作
void _readTextFromFile() async {
File file = File(await _getFilePath());
if (await file.exists()) {
setState(() => _fileContent = file.readAsStringSync()); // 文件存在則直接顯示文本信息
} else {
setState(() => _fileContent = ''); // 文件不存在則清空顯示文本信息羹饰,并提示
Fluttertoast.showToast(msg: '文件還未創(chuàng)建伊滋,請先通過寫入信息來創(chuàng)建文件');
}
}
因為外部存儲的文件需要涉及到權(quán)限問題碳却,而且 iOS 也不支持,所以如果需要使用文件來持久化數(shù)據(jù)的話笑旺,盡量使用另外兩種昼浦。因為在例子中,我們保存的數(shù)據(jù)相對比較簡單筒主,所以這邊就不得不說另外一種更方便的持久化方式了 shared_preferences
SharedPreferences
寫 Android 的小伙伴對這個應(yīng)該不陌生了关噪,但是 Flutter
并沒有自帶的 shared_preferences
功能,需要第三方插件來實現(xiàn)乌妙,引入 shared_preferences
插件使兔,寫文章的時候最新版本是 ^0.5.1+2
,還是先看下最后的效果
代碼的實現(xiàn)相對比較簡單
Widget _sharedPart() {
return Card(
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child:
Text('Shared Preferences', style: TextStyle(fontSize: 20.0, color: Theme.of(context).primaryColor)),
),
Padding(
padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0),
// 用于設(shè)置 key 信息
child: TextField(
controller: _shareKeyController,
decoration: InputDecoration(labelText: '輸入 share 存儲的 key', icon: Icon(Icons.lock_outline)),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0),
// 用于寫入文本信息
child: TextField(
controller: _shareValueController,
decoration: InputDecoration(labelText: '輸入 share 存儲的 value', icon: Icon(Icons.text_fields)),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0),
width: MediaQuery.of(context).size.width,
child: RaisedButton(
onPressed: _writeIntoShare,
child: Text('寫入 share'),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Text('share 存儲內(nèi)容:'), Expanded(child: Text(_shareContent, softWrap: true))],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0),
width: MediaQuery.of(context).size.width,
child: RaisedButton(
onPressed: _readFromShare,
child: Text('讀取 share'),
),
),
],
));
}
實現(xiàn)的關(guān)鍵部分就是方法 _writeIntoShare
和 _readFromShare
void _writeIntoShare() async {
var shareKey = _shareKeyController.value.text;
var shareContent = _shareValueController.value.text;
if (shareKey == null || shareKey.isEmpty) {
Fluttertoast.showToast(msg: '請輸入 key');
} else if (shareContent == null || shareContent.isEmpty) {
Fluttertoast.showToast(msg: '請輸入保存的內(nèi)容');
} else {
// 通過 `getInstance` 獲取 `shared_preferences` 單例
var sp = await SharedPreferences.getInstance();
// sp 能保存的數(shù)據(jù)類型包括 `int`, `String`, `bool`, `double`, `StringList`
sp.setString(shareKey, shareContent);
}
}
void _readFromShare() async {
var shareKey = _shareKeyController.value.text;
if (shareKey == null || shareKey.isEmpty) {
Fluttertoast.showToast(msg: '請輸入 key');
} else {
var sp = await SharedPreferences.getInstance();
// 數(shù)據(jù)讀取的類型同寫入類型藤韵,如果傳入的 key 不存在則返回 null
var value = sp.getString(shareKey);
if (value == null) {
Fluttertoast.showToast(msg: '未找到該 key');
setState(() => _shareContent = '');
} else {
setState(() => _shareContent = value);
}
}
}
這兩種數(shù)據(jù)持久化的方式主要用于存儲相對簡單虐沥,關(guān)系不復(fù)雜的數(shù)據(jù),如果涉及到大量的泽艘,且字段之間有關(guān)系的情況就需要通過數(shù)據(jù)庫來實現(xiàn)了欲险,Android 和 iOS 都自帶 sqlite 數(shù)據(jù)庫。
以上代碼查看 data_persistence_main.dart
文件
Sqflite
Flutter
實現(xiàn)數(shù)據(jù)庫存儲需要通過插件 sqflite
來實現(xiàn)悉盆,寫文章的時候最新的版本是 sqflite 1.1.3
盯荤,但是該版本需要 flutter 1.2
以上才行,所以我選擇的是 sqflite 1.1.0
焕盟,小伙伴可以根據(jù)自己的 flutter
版本選擇相應(yīng)的 sqflite
版本
sqflite 的基本操作語句秋秤,在文檔中已經(jīng)寫得非常明白了,所以就不搬運了脚翘,這邊直接講下對于數(shù)據(jù)庫的一些封裝處理吧灼卢,因為打開數(shù)據(jù)庫是一個很消耗資源的一個過程,所以呢来农,推薦實現(xiàn)單例會比較好鞋真。例如我們要實現(xiàn)一個 student
存儲表
class DatabaseUtils {
final String _tableStudent = 'student';
static Database _database; // 創(chuàng)建單例,防止重復(fù)打開消耗內(nèi)存
static DatabaseUtils _instance;
static DatabaseUtils get instance => _instance;
DatabaseUtils._internal() {
getDatabasesPath().then((path) async {
_database = await openDatabase(join(path, 'demo.db'), version: 2, onCreate: (db, version) {
// 創(chuàng)建數(shù)據(jù)庫的時候在這邊調(diào)用
db.execute('create table $_tableStudent '
'id integer primary key autoincrement,'
'name text not null,'
'age integer not null default 0,'
'gender integer not null default 0');
// 更新升級增加的字段
db.execute('alter table $_tableStudent add column birthday text');
}, onUpgrade: (db, oldVersion, newVersion) {
// 更新升級數(shù)據(jù)庫的時候在這操作
if (oldVersion == 1) db.execute('alter table $_tableStudent add column birthday text');
}, onOpen: (db) {
// 打開數(shù)據(jù)庫時候的回調(diào)
print('${db.path}');
});
});
}
factory DatabaseUtils() {
// 如果當(dāng)前的單例已經(jīng)存在沃于,則不再創(chuàng)建涩咖,否則重新創(chuàng)建,factory 關(guān)鍵詞看第一章
if (_instance == null) _instance = DatabaseUtils._internal();
return _instance;
}
}
那么對數(shù)據(jù)庫的操作就完全考驗?zāi)愕?SQL
的掌握程度了繁莹,但是千萬記住檩互,sqlite 中的類型只有,整型 integer
咨演,字符類型 text
闸昨,浮點類型 real
,二進制 blob
。數(shù)據(jù)庫的具體例子會等到最后的實際項目中展示饵较,原諒我不懂如何展示一個界面給你操作拍嵌,實現(xiàn)數(shù)據(jù)庫的各種功能。
該部分代碼查看 db_util.dart
文件循诉,里面有一些基本的操作寫法横辆,小伙伴可自行查看。
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個項目打洼,實現(xiàn)BLoC
模式龄糊,實現(xiàn)狀態(tài)管理:flutter_weather一個課程(當(dāng)時買了想看下代碼規(guī)范的逆粹,代碼更新會比較慢募疮,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改僻弹,很多地方看著不舒服阿浓,然后就改成自己的實現(xiàn)方式了):flutter_shop
如果對你有幫助的話,記得給個 Star蹋绽,先謝過芭毙,你的認(rèn)可就是支持我繼續(xù)寫下去的動力~