Day29 Flutter Persistence

今天我们来介绍几个 Persistence 的方法,即是用来储存数据,将数据存在我们的手机等硬体里,以便我们在重开App 或重新开机後也能够使用之前保存的数据的方法

SQLite

相比於其他储存资料於本地的方法,SQLite 资料库能够提供更为迅速的插入、更新、查询功能

之後范例会用到一些基本的SQL语句,如果你对於SQLite和SQL的各种语句还不熟悉,请查看SQLite官方的教程 SQLite教程

首先添加依赖:

  • sqflite :使用SQLite 资料库
  • path:以便能正确的定义资料库在硬体上的储存位置
...
dependencies:
	sqflite: ^1.3.1
  path: ^1.7.0
...

范例:

定义要储存的资料结构

新建一个要储存的Model 类,之後存在资料库里的资料就会有这些栏位

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});
}

打开资料库

在你准备读写资料库的数据之前,你要先打开这个资料库,此时需要以下两个步骤才可以打开资料库:

  1. 使用sqflitepackage里的getDatabasesPath方法并配合pathpackage里的 join方法定义资料库的路径
  2. 使用sqflite中的openDatabase()功能打开资料库

为了使用关键字await,必须将代码放在async函数内。您应该将以下所有表函数放在内void main() async {}

// Avoid errors caused by flutter upgrade.
// Importing 'package:flutter/widgets.dart' is required.
WidgetsFlutterBinding.ensureInitialized();
// Open the database and store the reference.
final Future<Database> database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
);

创建资料表

接下来,你需要创建一个表用以储存各种狗的信息,在此范例中,要创建一个名为dogs资料库表,它定义了可以被储存的数据。这样,每笔Dog数据就包含了一个idnameage,因此,在dogs数据库表中将有三列,分别是idnameage

  • idint类型,在资料表中是SQLite的INTEGER数据类型,推荐将id作为资料库表的主键,用以改善查询和修改的时间,另外name是Dart的String类型,在资料表中是SQLite的TEXT数据类型,age也是Dart的int类型,在资料表中是SQLite的INTEGER数据类型

    关於SQLite资料库能够储存的更多资料类型请查阅官方的 SQLite Datatypes文档

final Future<Database> database = openDatabase(
  // Set the path to the database. Note: Using the `join` function from the
  // `path` package is best practice to ensure the path is correctly
  // constructed for each platform.
  join(await getDatabasesPath(), 'doggie_database.db'),
  // When the database is first created, create a table to store dogs.
  onCreate: (db, version) {
    // Run the CREATE TABLE statement on the database.
    return db.execute(
      "CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
    );
  },
  // Set the version. This executes the onCreate function and provides a
  // path to perform database upgrades and downgrades.
  version: 1,
);

插入一笔资料

要在dogs资料表中插入一笔Dog的资料,需要分为以下两步:

  1. Dog转换成一个Map资料类型

  2. 使用insert()方法把Map保存到`dogs资料表中

// Update the Dog class to include a `toMap` method.
class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});

  // Convert a Dog into a Map. The keys must correspond to the names of the
  // columns in the database.
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }
}
// Define a function that inserts dogs into the database
Future<void> insertDog(Dog dog) async {
  // Get a reference to the database.
  final Database db = await database;

  // Insert the Dog into the correct table. You might also specify the
  // `conflictAlgorithm` to use in case the same dog is inserted twice.
  //
  // In this case, replace any previous data.
  await db.insert(
    'dogs',
    dog.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}
// Create a Dog and add it to the dogs table.
final fido = Dog(
  id: 0,
  name: 'Fido',
  age: 35,
);

await insertDog(fido);

查询资料表

现在已经有了一笔Dog资料存在资料库里,你可以通过查询资料库,检索到一只狗的资料或者所有狗的资料,分为以下两步:

  1. 调用dogs表对像的query方法,这将返回一个List <Map>
  2. List<Map>转换成List<Dog>资料类型
// A method that retrieves all the dogs from the dogs table.
Future<List<Dog>> dogs() async {
  // Get a reference to the database.
  final Database db = await database;

  // Query the table for all The Dogs.
  final List<Map<String, dynamic>> maps = await db.query('dogs');

  // Convert the List<Map<String, dynamic> into a List<Dog>.
  return List.generate(maps.length, (i) {
    return Dog(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

修改一笔资料

使用sqflitepackage中的update()方法,可以对已经插入到资料库中的数据进行修改(更新)

修改数据操作包含以下两步:

  1. 将一笔狗的数据转换成Map资料类型;
  2. 使用 where语句定位到具体将要被修改的资料
Future<void> updateDog(Dog dog) async {
  // Get a reference to the database.
  final db = await database;

  // Update the given Dog.
  await db.update(
    'dogs',
    dog.toMap(),
    // Ensure that the Dog has a matching id.
    where: "id = ?",
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [dog.id],
  );
}
// Update Fido's age.
await updateDog(Dog(
  id: 0,
  name: 'Fido',
  age: 42,
));

// Print the updated results.
print(await dogs()); // Prints Fido with age 42.

使用whereArgs将参数传递给where语句,有助於防止SQL注入攻击

这里请勿使用字串插补 (String interpolation),比如:where: "id = ${dog.id}"

删除一笔资料

除了插入和修改狗狗们的数据,你还可以从资料库中删除狗的数据。删除数据用到了sqflitepackage中的delete()方法。

在此范例,新建一个方法用来接收一个id并且删除资料库中与这个id匹配的那一笔资料。为了达到这个目的,你必须使用where语句限定哪一笔才是要被删除的资料

Future<void> deleteDog(int id) async {
  // Get a reference to the database.
  final db = await database;

  // Remove the Dog from the Database.
  await db.delete(
    'dogs',
    // Use a `where` clause to delete a specific dog.
    where: "id = ?",
    // Pass the Dog's id as a whereArg to prevent SQL injection.
    whereArgs: [id],
  );
}

完整的Sqlite 范例:

import 'dart:async';

import 'package:flutter/widgets.dart';

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

void main() async {
  // Avoid errors caused by flutter upgrade.
  // Importing 'package:flutter/widgets.dart' is required.
  WidgetsFlutterBinding.ensureInitialized();
  // Open the database and store the reference.
  final Future<Database> database = openDatabase(
    // Set the path to the database. Note: Using the `join` function from the
    // `path` package is best practice to ensure the path is correctly
    // constructed for each platform.
    join(await getDatabasesPath(), 'doggie_database.db'),
    // When the database is first created, create a table to store dogs.
    onCreate: (db, version) {
      return db.execute(
        "CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
      );
    },
    // Set the version. This executes the onCreate function and provides a
    // path to perform database upgrades and downgrades.
    version: 1,
  );

  Future<void> insertDog(Dog dog) async {
    // Get a reference to the database.
    final Database db = await database;

    // Insert the Dog into the correct table. Also specify the
    // `conflictAlgorithm`. In this case, if the same dog is inserted
    // multiple times, it replaces the previous data.
    await db.insert(
      'dogs',
      dog.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<List<Dog>> dogs() async {
    // Get a reference to the database.
    final Database db = await database;

    // Query the table for all The Dogs.
    final List<Map<String, dynamic>> maps = await db.query('dogs');

    // Convert the List<Map<String, dynamic> into a List<Dog>.
    return List.generate(maps.length, (i) {
      return Dog(
        id: maps[i]['id'],
        name: maps[i]['name'],
        age: maps[i]['age'],
      );
    });
  }

  Future<void> updateDog(Dog dog) async {
    // Get a reference to the database.
    final db = await database;

    // Update the given Dog.
    await db.update(
      'dogs',
      dog.toMap(),
      // Ensure that the Dog has a matching id.
      where: "id = ?",
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [dog.id],
    );
  }

  Future<void> deleteDog(int id) async {
    // Get a reference to the database.
    final db = await database;

    // Remove the Dog from the database.
    await db.delete(
      'dogs',
      // Use a `where` clause to delete a specific dog.
      where: "id = ?",
      // Pass the Dog's id as a whereArg to prevent SQL injection.
      whereArgs: [id],
    );
  }

  var fido = Dog(
    id: 0,
    name: 'Fido',
    age: 35,
  );

  // Insert a dog into the database.
  await insertDog(fido);

  // Print the list of dogs (only Fido for now).
  print(await dogs());

  // Update Fido's age and save it to the database.
  fido = Dog(
    id: fido.id,
    name: fido.name,
    age: fido.age + 7,
  );
  await updateDog(fido);

  // Print Fido's updated information.
  print(await dogs());

  // Delete Fido from the database.
  await deleteDog(fido.id);

  // Print the list of dogs (empty).
  print(await dogs());
}

class Dog {
  final int id;
  final String name;
  final int age;

  Dog({this.id, this.name, this.age});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }

  // Implement toString to make it easier to see information about
  // each dog when using the print statement.
  @override
  String toString() {
    return 'Dog{id: $id, name: $name, age: $age}';
  }
}

/*印出 
[Dog{id: 0, name: Fido, age: 35}]
[Dog{id: 0, name: Fido, age: 42}]
[]
*/

key-value

如果要储存的资料较少,我们则可以用shared_preferences插件,来将资料存在我们的手机等硬体里

shared_preferences插件可以把key-value的资料保存到手机等硬体中,并通过封装iOS上的NSUserDefaults和Android上的SharedPreferences为简单的数据提供持久化储存

使用key-value虽然方便,但它仅限用於基本资料类型:intdoubleboolstringstringList,还有它并不适用於大量资料的存取

首先添加依赖:shared_preferences

...
dependencies:
  shared_preferences: ^0.5.12
...

储存资料

使用SharedPreferences类的setter方法,Setter方法可用於各种基本资料类型,例如setIntsetBoolsetString

// obtain shared preferences
final prefs = await SharedPreferences.getInstance();

// set value
prefs.setInt('counter', counter);

读取资料

使用SharedPreferences类相应的getter方法。对於每一个setter方法都有对应的getter方法。例如,你可以使用getIntgetBoolgetString方法

final prefs = await SharedPreferences.getInstance();

// Try reading data from the counter key. If it doesn't exist, return 0.
final counter = prefs.getInt('counter') ?? 0;

删除资料

使用remove()方法删除数据

final prefs = await SharedPreferences.getInstance();

prefs.remove('counter');

完整的key-value 范例:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of the application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shared preferences demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Shared preferences demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _loadCounter();
  }

  //Loading counter value on start
  _loadCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = (prefs.getInt('counter') ?? 0);
    });
  }

  //Incrementing counter after click
  _incrementCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = (prefs.getInt('counter') ?? 0) + 1;
      prefs.setInt('counter', _counter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Read and write files

有时候我们会需要在local 端的文件做读写的操作,常见於App启动期间产生的持久化数据,或者从网络下载的数据要供离线使用

为了将文件保存到手机等硬体上,你需要结合使用 dart:io库中的path_provider这个package

首先添加依赖:path_provider

...
dependencies:
  path_provider: ^1.6.18
...

范例:

我们将会显示一个计数器,当计数器发生变化时,你将在硬体中写入资料,以便在App加载时重新读取这些数据

path_providerpackage 提供一种与平台无关的方式,以一致的方式访问设备的文件位置系统。该plugin 当前支持访问两种文件位置系统:

  1. Temporary directory (临时文件夹):

    这是一个系统可以随时清空的临时(缓存)文件夹。在iOS上对应NSCachesDirectory的返回值;在Android上对应getCacheDir()的返回值

  2. Documents directory (Documents目录):

    仅供app 使用,用於储存只能由该app使用的文件。只有在删除app时,系统才会清除这个目录。在iOS上,这个目录对应於NSDocumentDirectory。在Android上,则是AppData目录

  1. 找到正确的本地路径

    Future<String> get _localPath async {
      final directory = await getApplicationDocumentsDirectory();
    
      return directory.path;
    }
    
  2. 创建一个指向文件位置的 reference

    Future<File> get _localFile async { //使用dart:io库的File类来实现
      final path = await _localPath;
      return File('$path/counter.txt');
    }
    
  3. 将资料写入文件

    现在你已经有了可以使用的File,接下来就可以使用这个文件来读写数据,首先,将一些数据写入该文件。由於使用了计数器,因此只需将整数存为字串格式,然後使用'$counter'即可调用

    Future<File> writeCounter(int counter) async {
      final file = await _localFile;
    
      // Write the file.
      return file.writeAsString('$counter');
    }
    
  4. 从文件读取资料

    Future<int> readCounter() async {
      try {
        final file = await _localFile;
    
        // Read the file.
        String contents = await file.readAsString();
    
        return int.parse(contents);
      } catch (e) {
        // If encountering an error, return 0.
        return 0;
      }
    }
    

完整的计数器读取、写入文件范例:

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Reading and Writing Files',
      home: FlutterDemo(storage: CounterStorage()),
    ),
  );
}

class CounterStorage {
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/counter.txt');
  }

  Future<int> readCounter() async {
    try {
      final file = await _localFile;

      // Read the file
      String contents = await file.readAsString();

      return int.parse(contents);
    } catch (e) {
      // If encountering an error, return 0
      return 0;
    }
  }

  Future<File> writeCounter(int counter) async {
    final file = await _localFile;

    // Write the file
    return file.writeAsString('$counter');
  }
}

class FlutterDemo extends StatefulWidget {
  final CounterStorage storage;

  FlutterDemo({Key key, @required this.storage}) : super(key: key);

  @override
  _FlutterDemoState createState() => _FlutterDemoState();
}

class _FlutterDemoState extends State<FlutterDemo> {
  int _counter;

  @override
  void initState() {
    super.initState();
    widget.storage.readCounter().then((int value) {
      setState(() {
        _counter = value;
      });
    });
  }

  Future<File> _incrementCounter() {
    setState(() {
      _counter++;
    });

    // Write the variable as a string to the file.
    return widget.storage.writeCounter(_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Reading and Writing Files')),
      body: Center(
        child: Text(
          'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

<<:  Day29 语法改革!零基础新手也能读懂的JS - JS30-26 Stripe Follow Along Nav

>>:  Laravel 实战经验分享 - Day29 剩下最後的两篇,该讲些什麽呢?

Day21:今天来聊一下Firewall的Evasion

最後倒数10天真的是什麽状况都有老婆下雨骑车雷铲,原定在家写的 实做LAB文章只能在医院用手机以注音...

Day4 HTML 语法简易介绍(一)

HTML 语法简易介绍 HTML 是 Hypertext Markup Language 的缩写,也...

【Day13】在Ezyme上装上相对应版本的适配器(Adapter)吧´・ᴗ・`

前面我们有大概提到Enzyme的优点及作用~ 这篇我们要直接来安装Enzyme和导入Enzyme来供...

[Day 25] Reactive Programming - Spring WebFlux(R2DBC)

前言 在上一个范例中,是写死回传的内容,显然在现实生活中应该是不会有公司让你可以这样做的,而当我们的...

[Day 27] 建立注册的画面及功能(十一) - Gmail设定(二)

今天要分享的是G-mail寄信的另外一种方式, 虽然比较麻烦, 但是比较安全. 解除人机验证锁定 进...