2019-11-09 Flutter 數據的持久化

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 回調,正確處理讀寫操作的時序關系。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容