编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Flutter 入门指北之数据持久化(flutter长列表性能优化)

wxchong 2024-10-09 20:57:34 开源技术 11 ℃ 0 评论

码个蛋(codeegg)第 660 次推文

作者:Kuky_xs

博客:https://www.jianshu.com/p/97c2dbcac3af

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 == || 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 == || shareKey.isEmpty) {
Fluttertoast.showToast(msg: '请输入 key');
} else if (shareContent == || 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 == || shareKey.isEmpty) {
Fluttertoast.showToast(msg: '请输入 key');
} else {
var sp = await SharedPreferences.getInstance;
// 数据读取的类型同写入类型,如果传入的 key 不存在则返回
var value = sp.getString(shareKey);

if (value == ) {
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 ,'
'age integer not default 0,'
'gender integer not 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 == ) _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)

结尾

往期Flutter系列文,保你一周掌握!(持续更新!!!)

  • Flutter从配置安装到填坑指南详解

  • Flutter 入门指北之 Dart

  • Flutter 入门指北之基础部件

  • Flutter 入门指北之快速搭建界面

  • Flutter 入门指北之常用布局

  • Flutter 入门指北之路由

  • Flutter 入门指北之输入处理(登录界面实战)

  • Flutter入门指北之Sliver 组件及NestedScrollView

  • Flutter 入门指北之弹窗和提示(干货)

  • Flutter 入门指北之手势处理和动画

  • Flutter 入门指北之状态管理,BLoC

近期文章:

  • 程序员臆想:如果编程语言都是妹纸,娶哪个好?

  • Android开源库分类整理,你能用到的都在这了!

  • 2019过去一半了,目标咋样了?

今日问题:

最近学什么?

快来码仔社群解锁新姿势吧!社群升级:Max你的学习效率

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表