该文已授权公众号 「码个蛋」,转载请指明出处
上节讲了状态管理,但是当 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: [
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: [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: [
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: [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,先谢过,你的认可就是支持我继续写下去的动力~