Flutter 入门指北之数据持久化

来自:Kuky_xs英文原文

Flutter系列又继续来了~

还记得上次讲到哪里么?忘记的来看一下:Flutter 入门指北之状态管理,BLoC


上节讲了状态管理,但是当 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;
 }
}


最后代码的地址还是要的:


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

    (https://github.com/kukyxs/flutter_arts_demos_app)

  2. 基于郭神 cool weather接口的一个项目,实现 BLoC模式,实现状态管理:flutter_weather

    https://github.com/kukyxs/flutter_weather

  3. 一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop

    https://github.com/kukyxs/flutter_shop

推荐↓↓↓
安卓开发
上一篇:Gradle这么弱还跑来面腾讯? 下一篇:Android开源库分类整理,你能用到的都在这了!