Flutter essential skills: easy to master local storage and database optimization skills!

Because of the network, App has a channel for information exchange with the outside world, and therefore has the ability to update data. However, the exchanged data is usually stored in memory, and once the application finishes running, the memory will be released and the data will disappear.

Therefore, we need to save the updated data in a certain form and through a certain carrier, so that when the application runs next time, the data can be read from the stored carrier, and data persistence is realized .

There are many application scenarios for data persistence. like:

  • The user's account login information needs to be saved for each authentication with the Web service
  • Downloaded pictures need to be cached to avoid reloading every time, wasting user traffic

Since Flutter only takes over the rendering layer, when it comes to the underlying behavior of the operating system such as storage, it also needs to rely on native Android and iOS. Therefore, similar to native development, Flutter provides three types according to the size and method of persistent data. Data persistence methods, namely files, SharedPreferences and databases.

1 file

A file is a set of ordered information stored on a certain medium (such as a disk) with a specified path and a file name. From its definition, in order to achieve data persistence in the form of files, we first need to determine one thing: where is the data? This means defining the storage path of the file.

Flutter provides two directories for file storage, namely the Temporary directory and the Documents directory :

  • The temporary directory is a directory that the operating system can clear at any time, and is usually used to store some unimportant temporary cache data. This directory corresponds to the value returned by NSTemporaryDirectory on iOS, and corresponds to the value returned by getCacheDir on Android.
  • The document directory is a directory that will be cleared only when the application is deleted, and is usually used to store important data files generated by the application. On iOS, this directory corresponds to NSDocumentDirectory, while on Android it corresponds to the AppData directory.

File reading and writing in Flutter

In the following code, I declare three functions respectively, namely the function of creating file directory, writing file function and reading file function. It should be noted here that since file reading and writing are very time-consuming operations, these operations need to be performed in an asynchronous environment. In addition, in order to prevent exceptions during file reading, we also need try-catch on the outer package:

//创建文件目录
Future<File> get _localFile async {
    
    
  final directory = await getApplicationDocumentsDirectory();
  final path = directory.path;
  return File('$path/content.txt');
}
//将字符串写入文件
Future<File> writeContent(String content) async {
    
    
  final file = await _localFile;
  return file.writeAsString(content);
}
//从文件读出字符串
Future<String> readContent() async {
    
    
  try {
    
    
    final file = await _localFile;
    String contents = await file.readAsString();
    return contents;
  } catch (e) {
    
    
    return "";
  }
}

With the file read and write function, we can read and write the content.txt file in the code. In the following code, after we write a string to this file, we read it out after a while:

writeContent("Hello World!");
...
readContent().then((value)=>print(value));

In addition to reading and writing strings, Flutter also provides the ability to read and write binary streams, which can support the reading and writing of binary files such as pictures and compressed packages. These contents are not the focus of this sharing. If you want to study in depth, you can refer to the official documents .

2 SharedPreferences

Files are more suitable for large and ordered data persistence. If we only need to cache a small amount of key-value pair information (such as recording whether the user has read the announcement, or simple counting), SharedPreferences can be used.

SharedPreferences will provide persistent storage for simple key-value pair data with native platform-related mechanisms, that is, use NSUserDefaults on iOS and SharedPreferences on Android.

Next, I will use an example to demonstrate how to read and write data through SharedPreferences in Flutter. In the following code, we persist the counter into SharedPreferences, and provide it with read method and incremental write method respectively.

The setter (setInt) method will update the key-value pair in memory synchronously, and then save the data to disk, so we don't need to call the update method to force the cache to be refreshed. Similarly, since time-consuming file reading and writing are involved, we must wrap these operations asynchronously:

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

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

After completing the encapsulation of the counter access method, we can update and persist the counter data in the code at any time. In the following code, we first read and print the counter data, then increment it, and read and print it again:

//读出counter数据并打印
_loadCounter().then((value)=>print("before:$value"));

//递增counter数据后,再次读出并打印
_incrementCounter().then((_) {
    
    
  _loadCounter().then((value)=>print("after:$value"));
});

As you can see, the use of SharedPreferences is very simple and convenient. However, it should be noted that only basic types of data can be stored in the form of key-value pairs, such as int, double, bool, and string.

3 databases

The use of SharedPrefernces is convenient, but this method is only suitable for scenarios where a small amount of data is persisted. We cannot use it to store large amounts of data, such as file content (file paths are acceptable).

If we need to persist a large amount of formatted data, and these data will be updated at a higher frequency, in order to consider further scalability, we usually choose sqlite database to deal with such scenarios. Compared with files and SharedPreferences, databases can provide faster and more flexible solutions for data reading and writing.

Next, I will introduce you how to use the database with an example.

Take the Student class as an example:

class Student{
    
    
  String id;
  String name;
  int score;
  //构造方法
  Student({
    
    this.id, this.name, this.score,});
  //用于将JSON字典转换成类对象的工厂类方法
  factory Student.fromJson(Map<String, dynamic> parsedJson){
    
    
    return Student(
      id: parsedJson['id'],
      name : parsedJson['name'],
      score : parsedJson ['score'],
    );
  }
}

The JSON class has a factory class method that can convert a JSON dictionary into a class object, and we can also provide an instance method that converts a class object into a JSON dictionary in turn. Because what is finally stored in the database is not an entity class object, but a dictionary composed of basic types such as strings and integers, so we can use these two methods to realize the reading and writing of the database. At the same time, we also defined three Student objects for subsequent insertion into the database:

class Student{
    
    
  ...
  //将类对象转换成JSON字典,方便插入数据库
  Map<String, dynamic> toJson() {
    
    
    return {
    
    'id': id, 'name': name, 'score': score,};
  }
}

var student1 = Student(id: '123', name: '张三', score: 90);
var student2 = Student(id: '456', name: '李四', score: 80);
var student3 = Student(id: '789', name: '王五', score: 85);

With the entity class as the object stored in the database, the next step is to create a database. In the following code, we have given a database storage address through the openDatabase function, and created a students table for storing Student objects through the database table initialization statement:

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
  },
  version: 1,
);

The above code belongs to the general database creation template, there are three places to pay attention to:

  1. When setting the database storage address, use the join method to splice the two addresses. The join method will use the operating system's path separator when splicing, so we don't need to care whether the path separator is "/" or "\".
  2. When creating the database, a version 1 is passed in, and there is also a version in the callback of the onCreate method. The two versions are equal.
  3. The database will only be created once, which means that the onCreate method will only be executed once during the life cycle of the application from installation to uninstallation. What if we want to change the storage fields of the database during the version upgrade process?

SQLite provides the onUpgrade method, and we can determine the upgrade strategy based on the oldVersion and newVersion passed in by this method. Wherein, the former represents the database version on the user's mobile phone, and the latter represents the current version of the database version. For example, our application has three versions: 1.0, 1.1, and 1.2. In 1.1, we upgraded the database version to 2. Considering that the user's upgrade sequence is not always continuous, it may be directly upgraded from 1.0 to 1.2, so we can compare the current version of the database with the database version on the user's mobile phone in the onUpgrade function, and formulate a database upgrade plan.

After the database is created, we can then insert the three previously created Student objects into the database. The insertion of the database needs to call the insert method. In the following code, we convert the Student object into JSON. After specifying the insertion conflict strategy (if the same object is inserted twice, the latter replaces the former) and the target database table , completed the insertion of the Student object:

Future<void> insertStudent(Student std) async {
    
    
  final Database db = await database;
  await db.insert(
    'students',
    std.toJson(),
    //插入冲突策略,新的替换旧的
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}
//插入3个Student对象
await insertStudent(student1);
await insertStudent(student2);
await insertStudent(student3);

After the data is inserted, we can then call the query method to get them out. It should be noted that when writing, we insert sequentially one by one, and when reading, we use batch reading (of course, you can also specify query rules to read specific objects). The read data is an array of JSON dictionaries, so we also need to convert it into an array of Students. Finally, don't forget to release the database resources:

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]));
}

//读取出数据库中插入的Student对象集合
students().then((list)=>list.forEach((s)=>print(s.name)));
//释放数据库资源
final Database db = await database;
db.close();

It can be seen that in the face of reading a large number of formatted data models, the database provides a faster and more flexible persistence solution.

In addition to basic database read and write operations, sqlite also provides advanced features such as update, delete, and transaction, which are no different from SQLite or MySQL on native Android and iOS, so I won’t repeat them here. You can refer to the API documentation of the sqflite plugin , or refer to the SQLite tutorial for specific usage methods.

4 Summary

First, I took you to learn about files, the most common way of persisting data. Flutter provides two types of directories, the temporary directory and the document directory. We can achieve data persistence by writing strings or binary streams according to actual needs.

Then, I told you about SharedPreferences through a small example, a storage solution for persistent small key-value pairs.

Finally, we learned about databases together. Focusing on how to persist an object to the database, I introduced to you the methods of creating, writing and reading the database. It can be seen that although there is a lot more preparatory work in the way of using the database, in the face of continuous changing needs, the adaptability and flexibility are stronger.

Data persistence is a CPU-intensive operation, so data access will involve a lot of asynchronous operations, so be sure to use asynchronous wait or register then callbacks to correctly handle the timing relationship of read and write operations.

FAQ

  1. Please introduce the applicable scenarios of the three persistent data storage methods of files, SharedPreferences and databases respectively.
  2. Our application has gone through three versions 1.0, 1.1 and 1.2. Among them, version 1.0 created a new database and created a Student table, and version 1.1 added a field age (ALTER TABLE students ADD age INTEGER) to the Student table. Please write the database upgrade code for version 1.1 and version 1.2.
//1.0版本数据库创建代码
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
  },
  version: 1,
);

Guess you like

Origin blog.csdn.net/qq_33589510/article/details/131365071