Flutter 入门指北(Part 12)之数据持久化

该文已授权公众号 「码个蛋」,转载请指明出处

上节讲了状态管理,但是当 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 后,数据也能正常读取显示,说明数据被保存下来了

file_io.gif

看下实现的代码,因为会涉及到多种方式,所以这边我把视图抽取出来实现

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

shared.gif

代码的实现相对比较简单

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,先谢过,你的认可就是支持我继续写下去的动力~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345