25 | 本地存储与数据库的使用和优化
下载APP
关闭
渠道合作
推荐作者
25 | 本地存储与数据库的使用和优化
2019-08-24 陈航 来自北京
《Flutter核心技术与实战》
课程介绍
讲述:陈航
时长11:42大小10.72M
你好,我是陈航。
在上一篇文章中,我带你一起学习了 Flutter 的网络编程,即如何建立与 Web 服务器的通信连接,以实现数据交换,以及如何解析结构化后的通信信息。
其中,建立通信连接在 Flutter 中有三种基本方案,包括 HttpClient、http 与 dio。考虑到 HttpClient 与 http 并不支持复杂的网络请求行为,因此我重点介绍了如何使用 dio 实现资源访问、接口数据请求与提交、上传及下载文件、网络拦截等高级操作。
而关于如何解析信息,由于 Flutter 并不支持反射,因此只提供了手动解析 JSON 的方式:把 JSON 转换成字典,然后给自定义的类属性赋值即可。
正因为有了网络,我们的 App 拥有了与外界进行信息交换的通道,也因此具备了更新数据的能力。不过,经过交换后的数据通常都保存在内存中,而应用一旦运行结束,内存就会被释放,这些数据也就随之消失了。
因此,我们需要把这些更新后的数据以一定的形式,通过一定的载体保存起来,这样应用下次运行时,就可以把数据从存储的载体中读出来,也就实现了数据的持久化。
数据持久化的应用场景有很多。比如,用户的账号登录信息需要保存,用于每次与 Web 服务验证身份;又比如,下载后的图片需要缓存,避免每次都要重新加载,浪费用户流量。
由于 Flutter 仅接管了渲染层,真正涉及到存储等操作系统底层行为时,还需要依托于原生 Android、iOS,因此与原生开发类似的,根据需要持久化数据的大小和方式不同,Flutter 提供了三种数据持久化方法,即文件、SharedPreferences 与数据库。接下来,我将与你详细讲述这三种方式。
文件
文件是存储在某种介质(比如磁盘)上指定路径的、具有文件名的一组有序信息的集合。从其定义看,要想以文件的方式实现数据持久化,我们首先需要确定一件事儿:数据放在哪儿?这,就意味着要定义文件的存储路径。
Flutter 提供了两种文件存储的目录,即临时(Temporary)目录与文档(Documents)目录:
临时目录是操作系统可以随时清除的目录,通常被用来存放一些不重要的临时缓存数据。这个目录在 iOS 上对应着 NSTemporaryDirectory 返回的值,而在 Android 上则对应着 getCacheDir 返回的值。
文档目录则是只有在删除应用程序时才会被清除的目录,通常被用来存放应用产生的重要数据文件。在 iOS 上,这个目录对应着 NSDocumentDirectory,而在 Android 上则对应着 AppData 目录。
接下来,我通过一个例子与你演示如何在 Flutter 中实现文件读写。
在下面的代码中,我分别声明了三个函数,即创建文件目录函数、写文件函数与读文件函数。这里需要注意的是,由于文件读写是非常耗时的操作,所以这些操作都需要在异步环境下进行。另外,为了防止文件读取过程中出现异常,我们也需要在外层包上 try-catch:
有了文件读写函数,我们就可以在代码中对 content.txt 这个文件进行读写操作了。在下面的代码中,我们往这个文件写入了一段字符串后,隔了一会又把它读了出来:
SharedPreferences
文件比较适合大量的、有序的数据持久化,如果我们只是需要缓存少量的键值对信息(比如记录用户是否阅读了公告,或是简单的计数),则可以使用 SharedPreferences。
SharedPreferences 会以原生平台相关的机制,为简单的键值对数据提供持久化存储,即在 iOS 上使用 NSUserDefaults,在 Android 使用 SharedPreferences。
接下来,我通过一个例子来演示在 Flutter 中如何通过 SharedPreferences 实现数据的读写。在下面的代码中,我们将计数器持久化到了 SharedPreferences 中,并为它分别提供了读方法和递增写入的方法。
这里需要注意的是,setter(setInt)方法会同步更新内存中的键值对,然后将数据保存至磁盘,因此我们无需再调用更新方法强制刷新缓存。同样地,由于涉及到耗时的文件读写,因此我们必须以异步的方式对这些操作进行包装:
在完成了计数器存取方法的封装后,我们就可以在代码中随时更新并持久化计数器数据了。在下面的代码中,我们先是读取并打印了计数器数据,随后将其递增,并再次把它读取打印:
可以看到,SharedPreferences 的使用方式非常简单方便。不过需要注意的是,以键值对的方式只能存储基本类型的数据,比如 int、double、bool 和 string。
数据库
SharedPrefernces 的使用固然方便,但这种方式只适用于持久化少量数据的场景,我们并不能用它来存储大量数据,比如文件内容(文件路径是可以的)。
如果我们需要持久化大量格式化后的数据,并且这些数据还会以较高的频率更新,为了考虑进一步的扩展性,我们通常会选用 sqlite 数据库来应对这样的场景。与文件和 SharedPreferences 相比,数据库在数据读写上可以提供更快、更灵活的解决方案。
接下来,我就以一个例子分别与你介绍数据库的使用方法。
我们以上一篇文章中提到的 Student 类为例:
JSON 类拥有一个可以将 JSON 字典转换成类对象的工厂类方法,我们也可以提供将类对象反过来转换成 JSON 字典的实例方法。因为最终存入数据库的并不是实体类对象,而是字符串、整型等基本类型组成的字典,所以我们可以通过这两个方法,实现数据库的读写。同时,我们还分别定义了 3 个 Student 对象,用于后续插入数据库:
有了实体类作为数据库存储的对象,接下来就需要创建数据库了。在下面的代码中,我们通过 openDatabase 函数,给定了一个数据库存储地址,并通过数据库表初始化语句,创建了一个用于存放 Student 对象的 students 表:
以上代码属于通用的数据库创建模板,有三个地方需要注意:
在设定数据库存储地址时,使用 join 方法对两段地址进行拼接。join 方法在拼接时会使用操作系统的路径分隔符,这样我们就无需关心路径分隔符究竟是“/”还是“\”了。
创建数据库时,传入了一个 version 1,在 onCreate 方法的回调里面也有一个 version。这两个 version 是相等的。
数据库只会创建一次,也就意味着 onCreate 方法在应用从安装到卸载的生命周期中只会执行一次。如果我们在版本升级过程中,想对数据库的存储字段进行改动又该如何处理呢?
sqlite 提供了 onUpgrade 方法,我们可以根据这个方法传入的 oldVersion 和 newVersion 确定升级策略。其中,前者代表用户手机上的数据库版本,而后者代表当前版本的数据库版本。比如,我们的应用有 1.0、1.1 和 1.2 三个版本,在 1.1 把数据库 version 升级到了 2。考虑到用户的升级顺序并不总是连续的,可能会直接从 1.0 升级到 1.2,因此我们可以在 onUpgrade 函数中,对数据库当前版本和用户手机上的数据库版本进行比较,制定数据库升级方案。
数据库创建好了之后,接下来我们就可以把之前创建的 3 个 Student 对象插入到数据库中了。数据库的插入需要调用 insert 方法,在下面的代码中,我们将 Student 对象转换成了 JSON,在指定了插入冲突策略(如果同样的对象被插入两次,则后者替换前者)和目标数据库表后,完成了 Student 对象的插入:
数据完成插入之后,接下来我们就可以调用 query 方法把它们取出来了。需要注意的是,写入的时候我们是一个接一个地有序插入,读的时候我们则采用批量读的方式(当然也可以指定查询规则读特定对象)。读出来的数据是一个 JSON 字典数组,因此我们还需要把它转换成 Student 数组。最后,别忘了把数据库资源释放掉:
可以看到,在面对大量格式化的数据模型读取时,数据库提供了更快、更灵活的持久化解决方案。
除了基础的数据库读写操作之外,sqlite 还提供了更新、删除以及事务等高级特性,这与原生 Android、iOS 上的 SQLite 或是 MySQL 并无不同,因此这里就不再赘述了。你可以参考 sqflite 插件的API 文档,或是查阅SQLite 教程了解具体的使用方法。
总结
好了,今天的分享就这里。我们简单回顾下今天学习的内容吧。
首先,我带你学习了文件,这种最常见的数据持久化方式。Flutter 提供了两类目录,即临时目录与文档目录。我们可以根据实际需求,通过写入字符串或二进制流,实现数据的持久化。
然后,我通过一个小例子和你讲述了 SharedPreferences,这种适用于持久化小型键值对的存储方案。
最后,我们一起学习了数据库。围绕如何将一个对象持久化到数据库,我与你介绍了数据库的创建、写入和读取方法。可以看到,使用数据库的方式虽然前期准备工作多了不少,但面对持续变更的需求,适配能力和灵活性都更强了。
数据持久化是 CPU 密集型运算,因此数据存取均会大量涉及到异步操作,所以请务必使用异步等待或注册 then 回调,正确处理读写操作的时序关系。
思考题
最后,我给你留下两道思考题吧。
请你分别介绍一下文件、SharedPreferences 和数据库,这三种持久化数据存储方式的适用场景。
我们的应用经历了 1.0、1.1 和 1.2 三个版本。其中,1.0 版本新建了数据库并创建了 Student 表,1.1 版本将 Student 表增加了一个字段 age(ALTER TABLE students ADD age INTEGER)。请你写出 1.1 版本及 1.2 版本的数据库升级代码。
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 1
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
24 | HTTP网络编程与JSON解析
下一篇
26 | 如何在Dart层兼容Android/iOS平台特定实现?(一)
精选留言(17)
- Geek_joestar2019-09-091.1 版本及 1.2 版本的数据库升级代码: final Future<Database> database = openDatabase( join(await getDatabasesPath(), 'students_database.db'), onCreate: (db, version) => db.execute( "CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER, age INTEGER)"), onUpgrade: (db, oldVersion, newVersion) { //dosth for migration print("old:$oldVersion,new:$newVersion"); switch (oldVersion) { case 1: db.execute("ALTER TABLE students ADD age INTEGER"); } }, version: 2, );展开
作者回复: 对,不过switch-case是需要有break的
3 - 许童童2019-08-24思考题: 1.文件用来存储图片,视频类的大文件,SharedPreferences 用来存储一些键值对,比如记住用户名,数据库用来存储类似表格有关系的数据行。 2. //1.1 版本数据库创建代码 final Future<Database> database = openDatabase( join(await getDatabasesPath(), 'students_database.db'), onCreate: (db, version)=> { if (version == '1.0') { db.execute("ALTER TABLE students ADD age INTEGER)"), } else { db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER, age INTEGER)"), } } version: 1, ); //1.2 版本数据库创建代码 final Future<Database> database = openDatabase( join(await getDatabasesPath(), 'students_database.db'), onCreate: (db, version)=> { if (version == '1.0') { db.execute("ALTER TABLE students ADD age INTEGER)"), } else if (version == '1.1') { } else { db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER, age INTEGER)"), } } version: 1, );展开
作者回复: 数据库创建的语句不对: 1.1.0,1.1和1.2传入的version完全一样,没办法区分数据库版本 2.数据库升级的代码不太适合用if-else判断具体版本去写适配策略,版本一多适配代码就乱了。建议用none break的switch-case去写
2 - ptlCoder2019-08-29数据库升级应该要比较两个版本的字段,防止数据丢失应该把旧的字段值拷贝到新的版本中
作者回复: 数据量大的话不建议拷贝,建议直接在原始表上新增字段
1 - 汪帅2019-08-24我想请问一下关于获取系统信息怎么做啊?例如通讯录,安装的APP信息等等
作者回复: 需要在原生代码宿主写方法通道来实现了。具体可以参考26节的内容
共 4 条评论1 - Geek_0611962022-01-06数据库升级 推荐使用 sqflite_migration插件进行版本管理
- 七年2021-11-29老师好,问个问题,壁纸类app,都是网络图片,怎么优化?如何缓存图片呢,那种缓存方案合适?
- 快到碗里来2021-11-16存储数据最终都是桥接到原生代码实现的吗
- Jinx2021-06-03请问需要考虑版本降级时,数据库的变化吗?
- ABC2020-12-15Flutter本身功能有限,特殊需求只能找插件实现。如果插件没有合适的,就只能用原生代码写了。前几天刚用Java写了一个Android的Widget,配合Flutter使用。
- Mike2020-10-23https://developer.android.google.cn/training/data-storage/files/external-scoped?hl=zh-cn 针对原生 Android 10 之后的存储方式变化,文章的内容不知道是否还适用?现在好多App 都得为这变化买单修改成本。
- 考拉在草丛2020-06-29写入之后,怎么找不到content.txt的文件具体在哪里
- IF-Processing2020-02-05请问,如果在程序中,需要连续访问数据库(多表)操作时,是不是把openDatabase这个操作在应用启动时直接完成比较好呢?如果一直持有database的对象,对性能是否有影响呢?或者,换个角度,每次访问时,都要打开数据库,这样操作,是否会有性能延迟呢?这种操作怎么平衡比较好?
- coc2019-12-16getApplicationDocumentsDirectory找不到这方法
作者回复: import 'package:path_provider/path_provider.dart';
- Geek_neterM2019-09-09老师,是不是我本地没有content.txt,一直报 no such file or directory
作者回复: 检查一下你的路径名是不是和例子一样哈
- Geek_neterM2019-09-09老师,是不是实现必须把content.txt 创建好呀。
作者回复: 不需要的
- 小水滴2019-08-28final Future<Database> database_1v = openDatabase( join(await getDatabasesPath(),'students_database.db'), onCreate: (db,version)=>db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER)"), version: 1 ); //1.1版本 final Future<Database> database_11v = openDatabase( join(await getDatabasesPath(),'students_database.db'), onCreate: (db,version) { switch(version){ case 1: db.execute("ALTER TABLE students ADD age INTEGER)"); break; default: db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER , age INTEGER"); } }, version: 11 ); //1.1版本 final Future<Database> database_12v = openDatabase( join(await getDatabasesPath(),'students_database.db'), onCreate: (db,version) { switch(version){ case 1: db.execute("ALTER TABLE students ADD age INTEGER)"); break; case 11: break; default: db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER , age INTEGER"); } }, version: 12 );展开
作者回复: 这里有一个bug:你这里的switch-case只能执行单条更新语句,如果数据库版本差的比较多,每个版本都有更新,这样就不行了 另外更正一个原文中的错误:数据库的升级是在onUpgrade回调函数中完成的
- davidzhou2019-08-26老师,flutter的sqlite有没有ORM的相关第三方库
作者回复: pub上有一些,没有特别好用的,不建议