Flutter入門指北(Part12)之數據持久化

NO IMAGE

該文已授權公眾號 「碼個蛋」,轉載請指明出處

上節講了狀態管理,但是當 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 在寫入前記得先申請權限喲,否則也是不行滴。

讀寫文件操作需要通過 DartIO 操作完成,這邊小夥伴們可以自己看文檔 File class,接著我們就直接通過例子來看文件實現數據持久化。先看下效果吧,最終重啟 App 後,數據也能正常讀取顯示,說明數據被保存下來了

Flutter入門指北(Part12)之數據持久化

看下實現的代碼,因為會涉及到多種方式,所以這邊我把視圖抽取出來實現

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,還是先看下最後的效果

Flutter入門指北(Part12)之數據持久化

代碼的實現相對比較簡單

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 文件,裡面有一些基本的操作寫法,小夥伴可自行查看。

最後代碼的地址還是要的:

  1. 文章中涉及的代碼:demos

  2. 基於郭神 cool weather 接口的一個項目,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規範的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然後就改成自己的實現方式了):flutter_shop

如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~

相關文章

fish_redux「食用指南」

JetpackNavigation分析

Flutter與Android的交互

Flutter入門指北(Part14)之實戰篇