該文已授權公眾號 「碼個蛋」,轉載請指明出處
上節講了狀態管理,但是當 App
重啟后,數據就都丟失了,這樣就比較尷尬了,什么都要重來,所以這節我們來講下數據持久化。數據持久化主要有如下方式
- 文件讀寫
-
shared_preferences
存儲 - 數據庫存儲
持久化的實現都需要通過三方插件來實現,接著會慢慢介紹三種實現方式
文件讀寫/ IO 操作
文件讀寫需要 path_provider
插件,寫這篇文章的時候,最新版本是 0.5.0+1
,小伙伴們可以根據官網最新的版本進行替換,導入后我們就可以來看下如何實現文件的讀寫了。path_provider
的源碼比較簡單,這邊就不單獨拎出來說了,可以自行查看。path_provider
用于獲取手機的存儲文件位置,一共有三個方法
-
getTemporaryDirectory
臨時目錄,在 Android 中對應的方法為getCacheDir
,而在 iOS 中對應為NSCachesDirectory
,可以通過系統檢測并清除 -
getApplicationDocumentsDirectory
緩存目錄,在 Android 中對應為AppData
文件夾,在 iOS 中對應為NSDocumentsDirectory
,只有當 App 被刪除才能被刪除 -
getExternalStorageDirectory
外部存儲目錄,只有在 Android 中有效,在 iOS 調用會拋出UnsupportedError
異常,不過 Android 在寫入前記得先申請權限喲,否則也是不行滴。
讀寫文件操作需要通過 Dart
的 IO
操作完成,這邊小伙伴們可以自己看文檔 File class,接著我們就直接通過例子來看文件實現數據持久化。先看下效果吧,最終重啟 App 后,數據也能正常讀取顯示,說明數據被保存下來了
看下實現的代碼,因為會涉及到多種方式,所以這邊我把視圖抽取出來實現
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 是單選按鈕部件,通過選擇不同的情況,創建不同目錄的文件
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: '輸入存儲的文本內容', 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('文件內容:'), 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('讀取文件信息'),
),
),
]),
);
}
關鍵的部分在于 _writeTextIntoFile
和 _readTextFromFile
兩個方法的實現。看下實現的代碼
// 如果寫入外部內存需要讀寫權限,這邊使用了第三方插件 `permission_handler`
void _writeTextIntoFile() async {
if (_currentValue == _radioText[2]) {
PermissionStatus status = await PermissionHandler().checkPermissionStatus(PermissionGroup.storage);
if (status == PermissionStatus.granted) // 如果是寫入外部存儲,則檢測權限狀態,同意則寫入
_writeContent();
else if (status == PermissionStatus.disabled) // 拒絕了提示手動打開
Fluttertoast.showToast(msg: '未打開相關權限');
else // 未同意則主動申請權限
PermissionHandler().requestPermissions([PermissionGroup.storage]);
} else // 不是寫入外部存儲直接寫入文件
_writeContent();
}
// 文本寫入文件
void _writeContent() async {
// 寫入文本操作
var text = _editController.value.text; // 獲取文本框的內容
File file = File(await _getFilePath()); // 獲取相應的文件
if (text == null || text.isEmpty) {
Fluttertoast.showToast(msg: '請輸入內容'); // 內容為空,則不寫入并提醒
} else {
// 內容不空,則判斷是否已經存在,存在先刪除,重新創建后寫入信息
if (await file.exists()) file.deleteSync();
file.createSync(); // createSync 是一個同步的創建過程
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: '文件還未創建,請先通過寫入信息來創建文件');
}
}
因為外部存儲的文件需要涉及到權限問題,而且 iOS 也不支持,所以如果需要使用文件來持久化數據的話,盡量使用另外兩種。因為在例子中,我們保存的數據相對比較簡單,所以這邊就不得不說另外一種更方便的持久化方式了 shared_preferences
SharedPreferences
寫 Android 的小伙伴對這個應該不陌生了,但是 Flutter
并沒有自帶的 shared_preferences
功能,需要第三方插件來實現,引入 shared_preferences
插件,寫文章的時候最新版本是 ^0.5.1+2
,還是先看下最后的效果
代碼的實現相對比較簡單
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),
// 用于設置 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 存儲內容:'), 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'),
),
),
],
));
}
實現的關鍵部分就是方法 _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: '請輸入保存的內容');
} else {
// 通過 `getInstance` 獲取 `shared_preferences` 單例
var sp = await SharedPreferences.getInstance();
// sp 能保存的數據類型包括 `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();
// 數據讀取的類型同寫入類型,如果傳入的 key 不存在則返回 null
var value = sp.getString(shareKey);
if (value == null) {
Fluttertoast.showToast(msg: '未找到該 key');
setState(() => _shareContent = '');
} else {
setState(() => _shareContent = value);
}
}
}
這兩種數據持久化的方式主要用于存儲相對簡單,關系不復雜的數據,如果涉及到大量的,且字段之間有關系的情況就需要通過數據庫來實現了,Android 和 iOS 都自帶 sqlite 數據庫。
以上代碼查看 data_persistence_main.dart
文件
Sqflite
Flutter
實現數據庫存儲需要通過插件 sqflite
來實現,寫文章的時候最新的版本是 sqflite 1.1.3
,但是該版本需要 flutter 1.2
以上才行,所以我選擇的是 sqflite 1.1.0
,小伙伴可以根據自己的 flutter
版本選擇相應的 sqflite
版本
sqflite 的基本操作語句,在文檔中已經寫得非常明白了,所以就不搬運了,這邊直接講下對于數據庫的一些封裝處理吧,因為打開數據庫是一個很消耗資源的一個過程,所以呢,推薦實現單例會比較好。例如我們要實現一個 student
存儲表
class DatabaseUtils {
final String _tableStudent = 'student';
static Database _database; // 創建單例,防止重復打開消耗內存
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) {
// 創建數據庫的時候在這邊調用
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) {
// 更新升級數據庫的時候在這操作
if (oldVersion == 1) db.execute('alter table $_tableStudent add column birthday text');
}, onOpen: (db) {
// 打開數據庫時候的回調
print('${db.path}');
});
});
}
factory DatabaseUtils() {
// 如果當前的單例已經存在,則不再創建,否則重新創建,factory 關鍵詞看第一章
if (_instance == null) _instance = DatabaseUtils._internal();
return _instance;
}
}
那么對數據庫的操作就完全考驗你的 SQL
的掌握程度了,但是千萬記住,sqlite 中的類型只有,整型 integer
,字符類型 text
,浮點類型 real
,二進制 blob
。數據庫的具體例子會等到最后的實際項目中展示,原諒我不懂如何展示一個界面給你操作,實現數據庫的各種功能。
該部分代碼查看 db_util.dart
文件,里面有一些基本的操作寫法,小伙伴可自行查看。
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個項目,實現BLoC
模式,實現狀態管理:flutter_weather一個課程(當時買了想看下代碼規范的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然后就改成自己的實現方式了):flutter_shop
如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~