flutter 持久化儲存-----資料庫sqflite|8月更文挑戰

__white發表於2021-08-01

Flutter中持久化儲存資料有多種方案, 一般常用的有 shared_preferencessqfite

  • shared_preferences: 包含NSUserDefaults(在iOS上)和SharedPreferences(在Android上),為簡單資料提供持久儲存。資料以非同步方式持久儲存到磁碟。

  • sqflite: 是一款輕量級的關係型資料庫,類似SQLite. 支援iOS和Android。適用於儲存資料庫 , 表型別的資料.

sqflite的使用

新增依賴:

作者所用版本為1.1.3

dependencies:
  ...
  sqflite: ^1.1.3
複製程式碼

為了方便,外面建立一個DatabaseHelper來封裝一些資料庫的相關操作:

先貼一下程式碼 ,然後我們逐行分析:

/*
 * author: Created by 李卓原 on 2019/3/12.
 * email: zhuoyuan93@gmail.com
 *
 */

import 'dart:async';

import 'package:path/path.dart';
import 'package:sale_aggregator_app/models/video.dart';
import 'package:sqflite/sqflite.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = new DatabaseHelper.internal();

  factory DatabaseHelper() => _instance;

  final String tableVideo = 'VideoTable';
  final String columnId = 'id';
  final String image = 'image';
  final String url = 'url';
  final String duration = 'duration';
  final String title = 'title';
  final String favoriteStatus = 'favorite_status';

  static Database _db;

  DatabaseHelper.internal();

  Future<Database> get db async {
    if (_db != null) {
      return _db;
    }
    _db = await initDb();

    return _db;
  }

  initDb() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'flashgo.db');

    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
    return db;
  }

  void _onCreate(Database db, int newVersion) async {
    await db.execute(
        'CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT)');
  }

  Future<int> insertVideo(Video video) async {
    var dbClient = await db;
    var result = await dbClient.insert(tableVideo, video.toJson());

    return result;
  }

  Future<List> selectVideos({int limit, int offset}) async {
    var dbClient = await db;
    var result = await dbClient.query(
      tableVideo,
      columns: [columnId, image, url, duration, title, favoriteStatus],
      limit: limit,
      offset: offset,
    );
    List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));
    return videos;
  }

  Future<int> getCount() async {
    var dbClient = await db;
    return Sqflite.firstIntValue(
        await dbClient.rawQuery('SELECT COUNT(*) FROM $tableVideo'));
  }

  Future<Video> getVideo(int id) async {
    var dbClient = await db;
    List<Map> result = await dbClient.query(tableVideo,
        columns: [columnId, image, url, duration, title, favoriteStatus],
        where: '$id = ?',
        whereArgs: [id]);

    if (result.length > 0) {
      return Video.fromSql(result.first);
    }

    return null;
  }

  Future<int> deleteNote(String images) async {
    var dbClient = await db;
    return await dbClient
        .delete(tableVideo, where: '$image = ?', whereArgs: [images]);
  }

  Future<int> updateNote(Video video) async {
    var dbClient = await db;
    return await dbClient.update(tableVideo, video.toJson(),
        where: "$columnId = ?", whereArgs: [video.id]);
  }

  Future close() async {
    var dbClient = await db;
    return dbClient.close();
  }
}

複製程式碼

可以看到我們在執行相關方法的時候都會先獲得db, db會執行一個initDb方法,用於建立資料庫和表


  initDb() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'flashgo.db');

    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
    return db;
  }
  
  void _onCreate(Database db, int newVersion) async {
    await db.execute(
        'CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT)');
  }
複製程式碼
  • getDatabasesPath() : 獲取預設資料庫位置(在Android上,它通常是data/data/<package_name>/databases,在iOS上,它是Documents目錄)
  • join(databasesPath, 'flashgo.db'): 相當於在上述方法獲取到的位置建立了一個名為flashgo的資料庫.
  • openDatabase: 按指定路徑開啟資料庫 , 路徑就是上面flash.db的路徑,version為資料庫版本號,onCreate是建立表的方法

然後是我定義的一個實體類,看一下程式碼:

class Video {
  int id;
  String image;
  String url;
  int duration;

  String title;
  bool favoriteStatus;

  Video(
      {this.id,
      this.image,
      this.url,
      this.duration,
      this.title,
      this.favoriteStatus});

  Video.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    image = json['image'];
    url = json['url'];
    duration = json['duration'];
    title = json['title'];
    favoriteStatus = json['favorite_status'];
  }

  Video.fromSql(Map<String, dynamic> json) {
    id = json['id'];
    image = json['image'];
    url = json['url'];
    duration = json['duration'];
    title = json['title'];
    favoriteStatus = json['favorite_status'] == 'true';
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['image'] = this.image;
    data['url'] = this.url;
    data['duration'] = this.duration;
    data['title'] = this.title;
    data['favorite_status'] = this.favoriteStatus;
    return data;
  }
}

複製程式碼

細心的同學可能看到了一個與眾不同的方法: fromSql , 其中的json['favorite_status'] 是字串型別, 為什麼不依然用bool型呢, 因為sqlite不支援bool型.

上文中,CREATE TABLE $tableVideo($columnId INTEGER PRIMARY KEY, $image TEXT, $url TEXT, $duration INTEGER, $title TEXT, $favoriteStatus TEXT) 這個建表方法可以看到 id用的是integer, 其他都是用的text.

我們看一下sqlite都支援哪些資料型別吧:


sql儲存資料型別

每個儲存在 SQLite 資料庫中的值都具有以下儲存類之一:

儲存類描述
NULL值是一個 NULL 值。
INTEGER值是一個帶符號的整數,根據值的大小儲存在 1、2、3、4、6 或 8 位元組中。
REAL值是一個浮點值,儲存為 8 位元組的 IEEE 浮點數字。
TEXT值是一個文字字串,使用資料庫編碼(UTF-8、UTF-16BE 或 UTF-16LE)儲存。
BLOB值是一個 blob 資料,完全根據它的輸入儲存。

資料庫和表已經準備就緒了,那麼該看一看它的增刪改查了

插入資料
  Future<int> insertVideo(Video video) async {
    var dbClient = await db;
    var result = await dbClient.insert(tableVideo, video.toJson());

    return result;
  }
複製程式碼

這裡是我封裝的一個插入資料的方法,引數是video的物件, 但是可以看到insert方法第二個引數是一個json資料,所以其實也可以直接傳遞json資料.而不是傳一個物件再轉成json.

查詢方法

Future<List> selectVideos({int limit, int offset}) async {
    var dbClient = await db;
    var result = await dbClient.query(
      tableVideo,
      columns: [columnId, image, url, duration, title, favoriteStatus],
      limit: limit,
      offset: offset,
    );
    List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));
    return videos;
  }
複製程式碼

主要是呼叫了query方法,先看一下原始碼:

Future<List<Map<String, dynamic>>> query(String table,
      {bool distinct,
      List<String> columns,
      String where,
      List<dynamic> whereArgs,
      String groupBy,
      String having,
      String orderBy,
      int limit,
      int offset});
複製程式碼

有一個必傳引數是表名, 然後有很多修飾, 比如 limit : 是要查詢多少條資料, offset :是從哪裡開始查. columns: 是要查詢哪幾列 where: 是查詢條件,這裡我是查詢所有的所以沒有設定.

limit 和offset 這兩個也是最常用的屬性,所以我封裝方法的時候允許設定這兩個引數. 如果你有多張表,多個列需要查詢,我建議各自封裝方法,不然的話,需要傳入的引數過於複雜便失去了封裝的意義.

這裡的查詢方法返回的是json資料,且要記住,是隻有integer和text型別的,所以想要bool一定要自己處理

List<Video> videos = [];
    result.forEach((item) => videos.add(Video.fromSql(item)));
複製程式碼

所以這裡,我新建了一個fromSql的方法,把查詢出來的json資料轉成我想要的型別的物件.

查詢單個
  Future<Video> getVideo(int id) async {
    var dbClient = await db;
    List<Map> result = await dbClient.query(tableVideo,
        columns: [columnId, image, url, duration, title, favoriteStatus],
        where: '$id = ?',
        whereArgs: [id]);

    if (result.length > 0) {
      return Video.fromSql(result.first);
    }

    return null;
  }
複製程式碼

思路同上,只是要多了一個where,即查詢條件,這裡我是根據id來查 所以只傳入了一個id引數. 返回查詢到的結果(json型別) . 如果查詢不到,則返回一個null.

更改資料

  Future<int> updateVideo(Video video) async {
    var dbClient = await db;
    return await dbClient.update(tableVideo, video.toJson(),
        where: "$columnId = ?", whereArgs: [video.id]);
  }
複製程式碼

這個程式碼的邏輯是

  1. 傳入更改後的資料
  2. 根據傳入的資料id找到對應資料
  3. 更新資料

刪除資料

  Future<int> deleteVideo(String images) async {
    var dbClient = await db;
    return await dbClient
        .delete(tableVideo, where: '$image = ?', whereArgs: [images]);
  }
複製程式碼

其實邏輯和查詢是一樣的,我這是根據image來查詢並刪除.也可以用id或者其他資料.

獲取資料的數量

  Future<int> getCount() async {
    var dbClient = await db;
    return Sqflite.firstIntValue(
        await dbClient.rawQuery('SELECT COUNT(*) FROM $tableVideo'));
  }
複製程式碼

此方法用來查詢表中有多少條資料. 這我用了rawQuery方法, 它是支援直接使用sql語句進行查詢的. 因為該結果返回一個列表,所以使用Sqflite.firstIntValue來獲取其中的第一個值.

關閉方法

  Future close() async {
    var dbClient = await db;
    return dbClient.close();
  }
複製程式碼

在操作執行完畢後 , 記得關閉資料庫.關閉之後無法再訪問資料庫.

以上是對程式碼的分析,下面看一下實際的使用:

  //把視訊列表存到資料庫以備用
  void saveVideos(List<Video> videos) async {
    var db = DatabaseHelper();
    videos.forEach((v) => db.insertVideo(v));
    db.close();
  }
複製程式碼

相關程式碼盡在[github]flutter_study/sqflite_page.dart at master · lizhuoyuan/flutter_study (github.com))

相關文章