在上一篇文章中,我帶你一起學習了 Flutter 的網絡編程,即如何建立與 Web 服務器的通信連接,以實現數據交換,以及如何解析結構化后的通信信息。
其中,建立通信連接在 Flutter 中有三種基本方案,包括 HttpClient、http 與 dio。考慮到 HttpClient 與 http 并不支持復雜的網絡請求行為,因此我重點介紹了如何使用 dio 實現資源訪問、接口數據請求與提交、上傳及下載文件、網絡攔截等高級操作。
而關于如何解析信息,由于 Flutter 并不支持反射,因此只提供了手動解析 JSON 的方式:把 JSON 轉換成字典,然后給自定義的類屬性賦值即可。
正因為有了網絡,我們的 App 擁有了與外界進行信息交換的通道,也因此具備了更新數據的能力。不過,經過交換后的數據通常都保存在內存中,而應用一旦運行結束,內存就會被釋放,這些數據也就隨之消失了。
因此,我們需要把這些更新后的數據以一定的形式,通過一定的載體保存起來,這樣應用下次運行時,就可以把數據從存儲的載體中讀出來,也就實現了**數據的持久化**。
數據持久化的應用場景有很多。比如,用戶的賬號登錄信息需要保存,用于每次與 Web 服務驗證身份;又比如,下載后的圖片需要緩存,避免每次都要重新加載,浪費用戶流量。
由于 Flutter 僅接管了渲染層,真正涉及到存儲等操作系統底層行為時,還需要依托于原生 Android、iOS,因此與原生開發類似的,根據需要持久化數據的大小和方式不同,Flutter 提供了三種數據持久化方法,即文件、SharedPreferences 與數據庫。接下來,我將與你詳細講述這三種方式。
## 文件
文件是存儲在某種介質(比如磁盤)上指定路徑的、具有文件名的一組有序信息的集合。從其定義看,要想以文件的方式實現數據持久化,我們首先需要確定一件事兒:數據放在哪兒?這,就意味著要定義文件的存儲路徑。
Flutter 提供了兩種文件存儲的目錄,即**臨時(Temporary)目錄與文檔(Documents)目錄**:
* 臨時目錄是操作系統可以隨時清除的目錄,通常被用來存放一些不重要的臨時緩存數據。這個目錄在 iOS 上對應著 NSTemporaryDirectory 返回的值,而在 Android 上則對應著 getCacheDir 返回的值。
* 文檔目錄則是只有在刪除應用程序時才會被清除的目錄,通常被用來存放應用產生的重要數據文件。在 iOS 上,這個目錄對應著 NSDocumentDirectory,而在 Android 上則對應著 AppData 目錄。
接下來,我通過一個例子與你演示如何在 Flutter 中實現文件讀寫。
在下面的代碼中,我分別聲明了三個函數,即創建文件目錄函數、寫文件函數與讀文件函數。這里需要注意的是,由于文件讀寫是非常耗時的操作,所以這些操作都需要在異步環境下進行。另外,為了防止文件讀取過程中出現異常,我們也需要在外層包上 try-catch:
~~~
// 創建文件目錄
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 "";
}
}
~~~
有了文件讀寫函數,我們就可以在代碼中對 content.txt 這個文件進行讀寫操作了。在下面的代碼中,我們往這個文件寫入了一段字符串后,隔了一會又把它讀了出來:
~~~
writeContent("Hello World!");
...
readContent().then((value)=>print(value));
~~~
除了字符串讀寫之外,Flutter 還提供了二進制流的讀寫能力,可以支持圖片、壓縮包等二進制文件的讀寫。這些內容不是本次分享的重點,如果你想要深入研究的話,可以查閱[官方文檔](https://api.flutter.dev/flutter/dart-io/File-class.html)。
## SharedPreferences
文件比較適合大量的、有序的數據持久化,如果我們只是需要緩存少量的鍵值對信息(比如記錄用戶是否閱讀了公告,或是簡單的計數),則可以使用 SharedPreferences。
SharedPreferences 會以原生平臺相關的機制,為簡單的鍵值對數據提供持久化存儲,即在 iOS 上使用 NSUserDefaults,在 Android 使用 SharedPreferences。
接下來,我通過一個例子來演示在 Flutter 中如何通過 SharedPreferences 實現數據的讀寫。在下面的代碼中,我們將計數器持久化到了 SharedPreferences 中,并為它分別提供了讀方法和遞增寫入的方法。
這里需要注意的是,setter(setInt)方法會同步更新內存中的鍵值對,然后將數據保存至磁盤,因此我們無需再調用更新方法強制刷新緩存。同樣地,由于涉及到耗時的文件讀寫,因此我們必須以異步的方式對這些操作進行包裝:
~~~
// 讀取 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);
}
~~~
在完成了計數器存取方法的封裝后,我們就可以在代碼中隨時更新并持久化計數器數據了。在下面的代碼中,我們先是讀取并打印了計數器數據,隨后將其遞增,并再次把它讀取打印:
~~~
// 讀出 counter 數據并打印
_loadCounter().then((value)=>print("before:$value"));
// 遞增 counter 數據后,再次讀出并打印
_incrementCounter().then((_) {
_loadCounter().then((value)=>print("after:$value"));
});
~~~
可以看到,SharedPreferences 的使用方式非常簡單方便。不過需要注意的是,以鍵值對的方式只能存儲基本類型的數據,比如 int、double、bool 和 string。
## 數據庫
SharedPrefernces 的使用固然方便,但這種方式只適用于持久化少量數據的場景,我們并不能用它來存儲大量數據,比如文件內容(文件路徑是可以的)。
如果我們需要持久化大量格式化后的數據,并且這些數據還會以較高的頻率更新,為了考慮進一步的擴展性,我們通常會選用 sqlite 數據庫來應對這樣的場景。與文件和 SharedPreferences 相比,數據庫在數據讀寫上可以提供更快、更靈活的解決方案。
接下來,我就以一個例子分別與你介紹數據庫的使用方法。
我們以上一篇文章中提到的 Student 類為例:
~~~
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'],
);
}
}
~~~
JSON 類擁有一個可以將 JSON 字典轉換成類對象的工廠類方法,我們也可以提供將類對象反過來轉換成 JSON 字典的實例方法。因為最終存入數據庫的并不是實體類對象,而是字符串、整型等基本類型組成的字典,所以我們可以通過這兩個方法,實現數據庫的讀寫。同時,我們還分別定義了 3 個 Student 對象,用于后續插入數據庫:
~~~
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);
~~~
有了實體類作為數據庫存儲的對象,接下來就需要創建數據庫了。在下面的代碼中,我們通過 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)"),
version: 1,
);
~~~
以上代碼屬于通用的數據庫創建模板,有兩個地方需要注意:
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 對象的插入:
~~~
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);
~~~
數據完成插入之后,接下來我們就可以調用 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]));
}
// 讀取出數據庫中插入的 Student 對象集合
students().then((list)=>list.forEach((s)=>print(s.name)));
~~~
可以看到,在面對大量格式化的數據模型讀取時,數據庫提供了更快、更靈活的持久化解決方案。
除了基礎的數據庫讀寫操作之外,sqlite 還提供了更新、刪除以及事務等高級特性,這與原生 Android、iOS 上的 SQLite 或是 MySQL 并無不同,因此這里就不再贅述了。你可以參考 sqflite 插件的[API 文檔](https://pub.dev/documentation/sqflite/latest/),或是查閱[SQLite 教程](http://www.sqlitetutorial.net/)了解具體的使用方法。
## 總結
好了,今天的分享就這里。我們簡單回顧下今天學習的內容吧。
首先,我帶你學習了文件,這種最常見的數據持久化方式。Flutter 提供了兩類目錄,即臨時目錄與文檔目錄。我們可以根據實際需求,通過寫入字符串或二進制流,實現數據的持久化。
然后,我通過一個小例子和你講述了 SharedPreferences,這種適用于持久化小型鍵值對的存儲方案。
最后,我們一起學習了數據庫。圍繞如何將一個對象持久化到數據庫,我與你介紹了數據庫的創建、寫入和讀取方法。可以看到,使用數據庫的方式雖然前期準備工作多了不少,但面對持續變更的需求,適配能力和靈活性都更強了。
數據持久化是 CPU 密集型運算,因此數據存取均會大量涉及到異步操作,所以請務必使用異步等待或注冊 then 回調,正確處理讀寫操作的時序關系。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/25_data_persistence)中,你可以下載下來,反復運行幾次,加深理解與記憶。
## 思考題
最后,我給你留下兩道思考題吧。
1. 請你分別介紹一下文件、SharedPreferences 和數據庫,這三種持久化數據存儲方式的適用場景。
2. 我們的應用經歷了 1.0、1.1 和 1.2 三個版本。其中,1.0 版本新建了數據庫并創建了 Student 表,1.1 版本將 Student 表增加了一個字段 age(ALTER TABLE students ADD age INTEGER)。請你寫出 1.1 版本及 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)"),
version: 1,
);
~~~
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略