2019-11-09 Flutter 数据的持久化

2019-11-09  本文已影响0人  kingwl110

Flutter 数据的持久化

数据持久化的应用场景有很多。比如,用户的账号登录信息需要保存,用于每次与 Web 服务验证身份;又比如,下载后的图片需要缓存,避免每次都要重新加载,浪费用户流量。

由于 Flutter 仅接管了渲染层,真正涉及到存储等操作系统底层行为时,还需要依托于原生 Android、iOS,因此与原生开发类似的,根据需要持久化数据的大小和方式不同,Flutter 提供了三种数据持久化方法,即文件、SharedPreferences 与数据库。接下来,就详细讲述这三种方式。

文件

文件是存储在某种介质(比如磁盘)上指定路径的、具有文件名的一组有序信息的集合。从
其定义看,要想以文件的方式实现数据持久化,我们首先需要确定一件事儿:数据放在哪
儿? 这,就意味着要定义文件的存储路径。

Flutter 提供了两种文件存储的目录,即临时(Temporary)目录文档(Documents) 目录:

临时目录是操作系统可以随时清除的目录,通常被用来存放一些不重要的临时缓存数据。 这个目录在 iOS 上对应着 NSTemporaryDirectory 返回的值,而在 Android 上则对应着 getCacheDir 返回的值。

文档目录则是只有在删除应用程序时才会被清除的目录,通常被用来存放应用产生的重要数据文件。在 iOS 上,这个目录对应着 NSDocumentDirectory,而在 Android 上则对 应着 AppData 目录。

接下来,我通过一个例子与你演示如何在 Flutter 中实现文件读写。
在下面的代码中,我分别声明了三个函数,即创建文件目录函数、写文件函数与读文件函
数。这里需要注意的是,由于文件读写是非常耗时的操作,所以这些操作都需要在异步环境
下进行。另外,为了防止文件读取过程中出现异常,我们也需要在外层包上 try-catch:
有了文件读写函数,我们就可以在代码中对 content.txt 这个文件进行读写操作了。

import 'dart:io';
import 'package:path_provider/path_provider.dart';

class DataTool {
  ///文件
  //创建文件目录
  static Future<File> getLocalFile() async {
    final directory = await getApplicationDocumentsDirectory();
    final path = directory.path;
    return File('$path/content.txt');
  }

  //将字符串写入文件
  static Future<File> writeFileToContent(String content) async {
    final file = await getLocalFile();
    return file.writeAsString(content);
  }

  // 从文件读出字符串
  static Future<String> readFileContent() async {
    try {
      final file = await getLocalFile();
      String contents = await file.readAsString();
      return contents;
    } catch (e) {
      return '';
    }
  }
}

除了字符串读写之外,Flutter 还提供了二进制流的读写能力,可以支持图片、压缩包等二进制文件的读写。如果你想要深入研究的话,可以查阅官方文档。

SharedPreferences

文件比较适合大量的、有序的数据持久化,如果我们只是需要缓存少量的键值对信息(比如 记录用户是否阅读了公告,或是简单的计数),则可以使用 SharedPreferences。

SharedPreferences 会以原生平台相关的机制,为简单的键值对数据提供持久化存储,即在 iOS 上使用 NSUserDefaults,在 Android 使用 SharedPreferences。

接下来,我通过一个例子来演示在 Flutter 中如何通过 SharedPreferences 实现数据的读 写。在下面的代码中,我们将计数器持久化到了 SharedPreferences 中,并为它分别提供了读方法和递增写入的方法。
这里需要注意的是,setter(setInt)方法会同步更新内存中的键值对,然后将数据保存至磁盘,因此我们无需再调用更新方法强制刷新缓存。同样地,由于涉及到耗时的文件读写, 因此我们必须以异步的方式对这些操作进行包装:

  // 读取 SharedPreferences 中 key 为 counter 的值
  Future<int> _readSPCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int counter = (prefs.getInt('counter') ?? 0);
    return counter;
  }

  //写入 SharedPreferences 中 key 为 counter 的值
  Future<void> _writeSPCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int counter = (prefs.getInt('counter') ?? 0) + 1;
    prefs.setInt('counter', counter);
  }

可以看到,SharedPreferences 的使用方式非常简单方便。不过需要注意的是,以键值对的方式只能存储基本类型的数据,比如 int、double、bool 和 string。

数据库

SharedPrefernces 的使用固然方便,但这种方式只适用于持久化少量数据的场景,我们并不能用它来存储大量数据,比如文件内容(文件路径是可以的)。
如果我们需要持久化大量格式化后的数据,并且这些数据还会以较高的频率更新,为了考虑进一步的扩展性,我们通常会选用 sqlite 数据库来应对这样的场景。与文件和 SharedPreferences 相比,数据库在数据读写上可以提供更快、更灵活的解决方案。
接下来,我就以一个例子分别与你介绍数据库的使用方法。 我们以上一篇文章中提到的 Student 类为例:
```

class Student {
  String id;
  String name;
  int score;

  Student({
    this.id,
    this.name,
    this.score,
  });

  factory Student.fromJson(Map<String, dynamic> parsedJson) {
    return Student(
      id: parsedJson['id'],
      name: parsedJson['name'],
      score: parsedJson['score'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'score': score,
    };
  }
}
```

JSON 类拥有一个可以将 JSON 字典转换成类对象的工厂类方法,我们也可以提供将类对象反过来转换成 JSON 字典的实例方法。因为最终存入数据库的并不是实体类对象,而是字符串、整型等基本类型组成的字典,所以我们可以通过这两个方法,实现数据库的读写。 同时,我们还分别定义了 3 个 Student 对象,用于后续插入数据库:

var student1 = Student(id: '${++studentID}', name: '张三', score: 90);
var student2 = Student(id: '${++studentID}', name: '李四', score: 80);
var student3 = Student(id: '${++studentID}', name: '王五', score: 85);

有了实体类作为数据库存储的对象,接下来就需要创建数据库了。在下面的代码中,我们通过 openDatabase 函数,给定了一个数据库存储地址,并通过数据库表初始化语句,创建 了一个用于存放 Student 对象的 students 表:

//创建数据库
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)"),
  onUpgrade: (db, oldVersion, newVersion) {
    //dosth for migration
    print("old:$oldVersion, new:$newVersion");
  },
  version: 1,
);

//插入数据方法
Future<void> insertStudent(Student std) async {
  final Database db = await database;
  await db.insert(
    'students',
    std.toJson(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

//插入数据
await insertStudent(student1);
await insertStudent(student2);
await insertStudent(student3);

以上代码属于通用的数据库创建模板,有两个地方需要注意:

  1. 在设定数据库存储地址时,使用 join 方法对两段地址进行拼接。join 方法在拼接时会 使用操作系统的路径分隔符,这样我们就无需关心路径分隔符究竟是“/”还 是“\”了。
  2. 在创建数据库时,传入了一个参数 version:1,在 onCreate 方法的回调里面也有一个 参数 version。前者代表当前版本的数据库版本,后者代表用户手机上的数据库版本。
    比如,我们的应用有 1.0、1.1 和 1.2 三个版本,在 1.1 把数据库 version 升级到了 2。考虑到用户的升级顺序并不总是连续的,可能会直接从 1.0 升级到 1.2。因此我们可以在onCreate 函数中,根据数据库当前版本和用户手机上的数据库版本进行比较,制定数据库升级方案。
    数据库创建好了之后,接下来我们就可以把之前创建的 3 个 Student 对象插入到数据库中 了。数据库的插入需要调用 insert 方法,在下面的代码中,我们将 Student 对象转换成了 JSON,在指定了插入冲突策略(如果同样的对象被插入两次,则后者替换前者)和目标数 据库表后,完成了 Student 对象的插入:

数据完成插入之后,接下来我们就可以调用 query 方法把它们取出来了。需要注意的是, 写入的时候我们是一个接一个地有序插入,读的时候我们则采用批量读的方式(当然也可以指定查询规则读特定对象)。读出来的数据是一个 JSON 字典数组,因此我们还需要把它 转换成 Student 数组:

//读取数据
Future<List<Student>> students() async {
  final Database db = await database;
  final List<Map<String, dynamic>> maps = await db.query('students');
  return List.generate(maps.length, (i) => Student.fromJson(maps[i]));
}

可以看到,在面对大量格式化的数据模型读取时,数据库提供了更快、更灵活的持久化解决
方案。
除了基础的数据库读写操作之外,sqlite 还提供了更新、删除以及事务等高级特性,这与原生 Android、iOS 上的 SQLite 或是 MySQL 并无不同,因此这里就不再赘述了。你可以参考 sqflite 插件的API 文档,或是查阅SQLite 教程了解具体的使用方法。

总结

首先,我带你学习了文件,这种最常见的数据持久化方式。Flutter 提供了两类目录,即临 时目录与文档目录。我们可以根据实际需求,通过写入字符串或二进制流,实现数据的持久化。
然后,我通过一个小例子和你讲述了 SharedPreferences,这种适用于持久化小型键值对 的存储方案。
最后,我们一起学习了数据库。围绕如何将一个对象持久化到数据库,我与你介绍了数据库
的创建、写入和读取方法。可以看到,使用数据库的方式虽然前期准备工作多了不少,但面
对持续变更的需求,适配能力和灵活性都更强了。
数据持久化是 CPU 密集型运算,因此数据存取均会大量涉及到异步操作,所以请务必使用异步等待或注册 then 回调,正确处理读写操作的时序关系。

上一篇下一篇

猜你喜欢

热点阅读