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);
以上代碼屬于通用的數據庫創建模板,有兩個地方需要注意:
- 在設定數據庫存儲地址時,使用 join 方法對兩段地址進行拼接。join 方法在拼接時會 使用操作系統的路徑分隔符,這樣我們就無需關心路徑分隔符究竟是“/”還 是“\”了。
- 在創建數據庫時,傳入了一個參數 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 回調,正確處理讀寫操作的時序關系。