Flutter 工程化框架選擇 — 搞定資料儲存選型

語言: CN / TW / HK

theme: smartblue

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

這是 《Flutter 工程化框架選擇》 系列的第三篇 ,就像之前說的,這個系列只是單純告訴你,建立一個 Flutter 工程,或者說搭建一個 Flutter 工程腳手架,應該如何快速選擇適合自己的功能模組,可以說這是一個指引系列,所以比較適合新手同學。

Flutter 上關於資料儲存或者資料庫詳細選型介紹的內容很少,也算是一個補全吧

本篇主要介紹資料儲存相關,可能就有人會覺得,資料儲存有什麼好說的?不就是寫個 Plugin 接個原生資料庫就好了嗎?這都能水?

確實,最簡單快捷的方法就是寫個 Plugin ,通過 Dart 直接呼叫原生平臺的資料儲存能力,這樣實現成本最低,但是對於 Flutter 來說可能不夠優雅:

  • 通過 MethodCallHandler 呼叫的方式,中間過程存在一定程度的效能消耗,特別是讀寫資料量比較大的時候
  • Flutter 需要多平臺支援,通過 MethodCallHandler 呼叫原生平臺,就需要在每個平臺的使用不同的程式碼和適配邏輯,會提高維護成本

所以針對 Flutter 平臺,現在社群的資料儲存工具實現都默契的採用了另外一種方式,當然不是說使用 MethodCallHandler 的 Plugin 實現不行,具體選型還是需要看你所需要的場景。

Let's go ⚠️ ~

Plugin 呼叫原生平臺實現

首先簡單介紹一下大家比較熟悉的兩個資料儲存的庫,它們都是通過 Plugin 直接呼叫原生平臺 API 進行資料儲存:

  • shared_preferences : key-value 的簡單資料儲存,相信 Android 開發對這個名稱會很親切,官方維護,實現簡單,支援全平臺,適合對效能要求不高和資料量不大的場景

不過也是因為 MethodCallHandler 呼叫的方式,從維護和呼叫路徑上會顯得比較複雜,federated plugin 的寫法和版本管理方式容易出現問題,已經不止一次在使用官方的 federated plugin 因為內部版本問題踩坑。

  • sqflite :可以說是最早的第三方 Flutter 資料庫之一,支援 iOS、 Android 和 MacOS ,為什麼不支援全平臺,這其實也是 Plugin 直接呼叫多平臺的問題之一,你需要重複在每個平臺上實現同一個邏輯,雖然初期開發成本低,但是後續維護成本並不低,同時 sqflite 提供的能力也比較弱,封裝程度不高,主要還是解決了早期對資料庫迫切支援的需求。

既然說到 sqflite ,這裡推薦一個 flutter-sqlite-viewer 的第三方工具,相信大家在操作資料的時候都有視覺化檢視的需求,雖然開發過程中可以如下左圖一樣在 Android Studio 裡通過 App Inspection 檢視,但是有一些場景你沒辦法提供直連 Debug 除錯,這時候 flutter-sqlite-viewer 就可以很方便提供本地化資料庫檢視的能力。

| | | | ----------------------------------------------------------- | -------------------------------------- |

sqlite3

從這裡開始我們介紹不大一樣的,sqlite3 採用的是 dart:ffi 實現直接通過 Dart 訪問資料庫的能力,那這裡的區別是什麼?

  • dart 和 c/c++ 的相關程式碼可以跨平臺,不需要維護多份同樣的平臺邏輯(雖然需要維護多份編譯產物)
  • dart 和資料庫直接互動,省去中間過程的轉換需要
  • 有問題了你不好除錯

其實從 Flutter 2.0 和 Dart 2.12 提供 FFI 支援開始,到現在的 Dart 3.0 正式版釋出,官方一直都在完善 ffi 還有和平臺語言直接互動的能力,例如在 Dart 2.18 裡 Dart 就支援與 Objective-C 和 Swift 直接互動

目前 sqlite3 支援 Android、iOS、Linux、MacOS、Window 平臺,並且在特定平臺還提供了切換到 SQLCipher 的支援:

  • Android 平臺可以依賴 sqlite3_flutter_libs 來整合最新的 sqlite3 版本,也可以通過依賴 sqlcipher_flutter_libs 來切換到 SQLCipher
  • iOS 平臺和 macOS 平臺與 Android 類似
  • Windows 和 Linux 平臺可以依賴 sqlite3_flutter_libs 來整合最新的 sqlite3 版本
  • Web 平臺其實也支援,只是只支援在 WASM 模式下,也就是 --web-renderer canvaskit 的時候使用,同時還需要一些額外的操作,例如把 sqlite3.wasm 放到 web/ 目錄下

當然其實 sqlite3 不只是支援 Flutter ,它的 ffi 也是 Dart 的能力,所以它也可以直接用在 dart server 上

所以在 sqlite3 裡 Dart 就是通過 dart:ffi 直接和資料庫互動,而 Plugin 裡的內容更多隻是一個初始化的作用,例如 sqlcipher_flutter_libs 的 Plugin 就是載入對應的 sqlcipher.so

Drift

可能是直接使用 sqlite3 還不夠優雅,所以作者針對 sqlite3 又封裝了一套 Drift ,Drift 同樣可以脫離 Flutter 執行,因為它底層 sqlite3 依然基於 dart:ffi ,支援 Android、iOS、macOS、Linux 、 Windows 和 web , 不同之處是它對事務、 migrations、複雜過濾、表示式、批量更新和 joins 等操作做了封裝,例如:

  • 根據註解生成對映程式碼
  • 對查詢結果根據 Stream 實現自動更新
  • 更方便的管理 schema migrations 和 CREATE TABLE

例如你可以通過如下所示進行聚合查詢,將多個數據結果合併到一起:

```dart Future countTodosInCategories() async { final amountOfTodos = todos.id.count();

final query = select(categories).join([ innerJoin( todos, todos.category.equalsExp(categories.id), useColumns: false, ) ]); query ..addColumns([amountOfTodos]) ..groupBy([categories.id]);

final result = await query.get();

for (final row in result) { print('there are ${row.read(amountOfTodos)} entries in' '${row.readTable(categories)}'); } }

Stream averageItemLength() { final avgLength = todos.content.length.avg(); final query = selectOnly(todos)..addColumns([avgLength]); return query.map((row) => row.read(avgLength)!).watchSingle(); }

```

當然,這並不是最有趣的,Drift 裡最有意思的是提供了 sql 解析器和分析器,所以你可以通過 sql 語句來生成 API,並且還會在構建時提供錯誤警告。

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

這裡順便提一嘴,在 Flutter 裡通過狀態管理工具往下傳遞 database 大家應該不陌生吧, drift 官方同樣建議使用 provider 或者 riverpod 往下傳遞 database 物件,例如:

dart void main() { runApp( Provider<MyDatabase>( create: (context) => MyDatabase(), child: MyFlutterApp(), dispose: (context, db) => db.close(), ), ); }

同時,針對 sqlite,作者還提供了相關的選擇建議:

  • 如果你不怕麻煩,想更輕量級,那麼可以直接使用 sqlite3
  • 如果你只需要 Stream 實現自動更新,不需要自動生成 dart 查詢對映,那可以選擇 sqlcool
  • 如果你需要和 Drift 功能類似,但是需要更靈活的自定義能力且不介意麻煩的話,可以選擇 floor

最後,如下圖所示,你還可以用 db_viewer 來預覽資料庫內容資料。

Realm

可能不少人對 realm 還並不瞭解,第一次接觸 realm 是在 2016 年開發 React Native 的時候,那時候 realm 幾乎就是我首選的資料庫,它作為 SQLite 的替代,採用的是 MongoDB 的資料庫方案。

如今 realm 也開始支援 Flutter ,雖然還在 Beta(已經 Beta 挺久了),但是它同樣採用了基於 dart:ffi 的實現,所以 realm 同樣支援脫離 Flutter 純 Dart 執行,並且支援 Android、iOS 、Linux、MacOS 、Windows 等平臺執行。

關於 MongoDB 和 SQL 的差異對比這裡就不說,主要就是關係型資料庫與非關係型資料庫的區別

那 realm 最大的特別之處是什麼?那就是它除了提供本地資料庫能力之外,它還提供資料同步和後端儲存能力

簡單來說,就是 realm 的資料庫支援實時同步,在此之前我設計的 React Native 專案裡,就有基於 realm 很快就開發出一套支援資料備份和同步的聊天應用的場景。

簡單介紹一下,就是在 realm 的後臺服務上,主要需要通過定義 Schema Table 和 sync 許可權,就可以通過 realm SDK 在啟動時同步資料,並對資料進行實時同步,而在 realm 上,你可以通過 CredentialsUser 來定義角色的讀寫許可權和管理資料。

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

如下圖所示,開啟 sync 之後,在 iPhone 模擬器上點選新增的任務,在 Android 模擬器上就會實時同步 iPhone 上對資料庫的操作更改,並且 realm 的 sync 後臺預設就支援叢集服務,如果使用者量不是特別大的情況下,拿來做聊天或者客服場景還是很可行的。

realm-dart-samples 裡同樣提供了對應的例子,但是它的例子有問題,所以你需要自己註冊 realm.io 上的服務,並且獲取到 appId 後,替換 appId 並在 realm 後臺建立自己的 Task 表,開啟 sync 服務。

同時 realm 也提供 realm-studio 支援資料庫視覺化的能力,當然,如果你使用了 realm 的資料同步服務,那麼在 Atlas 上也可以實時看到對應的資料更新。

| | | | ------------------------------------------------ | ----------------------------------------------------------- |

不過還是那句話,目前 realm 還在 beta ,例如:

  • 很多 API 上可以看到文件緊缺
  • 不支援 reload ,sync 模式下一 reload 就crash

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

另外,比如我不說,你翻閱文件也很難找到 Flutter 上如何監聽和同步查詢結果變化,而其實這部分程式碼其實你只需要通過 stream 就可以接入實現。

dart StreamBuilder<RealmResultsChanges<Task>>( stream: MyApp.allTasksRealm.all<Task>().changes, builder: (c, s) { return Text((s.data != null) ? s.data!.results.length.toString() : "null", style: Theme.of(context).textTheme.headline4); })

所以目前在 Flutter 上,如果在其他平臺之前用過 realm ,那可以試試;但是如果沒有,建議先不躺坑,等正式版吧。

其實和 Realm 一樣具有實時同步能力,同樣是基於檔案的非關係資料庫,並且更穩定更可靠的資料庫是 firebase_database ,不過周所周知的歡迎,國內基本不會選擇它。

ObjectBox

ObjectBox 其實和 Realm 類似,也是 NoSQL 型別的資料庫,同樣是基於 dart:ffi ,支援 Android 、iOS 、Linux 、MacOS 和 Window,號稱在能耗和速度上有絕對的優勢,同時因為是純 Dart API,所以它完全不需要你熟悉或者學習 SQL 語法

```dart @Entity() class Person { int id;

String firstName; String lastName;

Person({this.id = 0, required this.firstName, required this.lastName}); }

final store = await openStore(); final box = store.box();

var person = Person(firstName: 'Joe', lastName: 'Green');

final id = box.put(person); // Create

person = box.get(id)!; // Read

person.lastName = "Black"; box.put(person); // Update

box.remove(person.id); // Delete

// find all people whose name start with letter 'J' final query = box.query(Person_.firstName.startsWith('J')).build(); final people = query.find(); // find() returns List ```

目前 objectbox 支援 dart 、java、kotlin 、swift 甚至還支援 GO 和 Python

在使用體驗上,ObjectBox 可能會更貼近 NoSQL 的操作習慣, 另外它也可以直接在服務端被使用,從官方提供的基準測試中看,ObjectBox 的整體效能確實很優秀(其中的 Hive 後面介紹)。

對測試感興趣的可以看 objectbox-dart-performance ,不要問我它是不是真的這麼好用,因為我也沒在生產專案上使用它。

ObjectBox 另外一個特點就是支援離線資料同步:ObjectBox Sync ,和 realm 還有 firebase 類似,這其實已經成為 NoSQL 服務商都會提供的特色之一。

ObjectBox Sync 離線同步的支援主要在:當裝置離線時資料操作會被儲存在本地,當裝置連線上網路時,資料會恢復同步

拋開 sync 能力不談,ObjectBox 作為本地資料庫在效能和開發體驗上真的很不錯。

Hive

前面介紹的都是比較重的資料庫型別,那接下來介紹個輕量型的儲存框架: Hive

Hive 是一個純 Dart 實現的輕量 key-value 資料庫,主要通過 dart/io 對檔案進行讀寫,支援 Android 、iOS、Linux、MacOS、Window、Web 平臺。

作為 key-value 資料庫,它其實很適合用來替代 shared_preferences ,並且它支援使用 AES 進行加密,目前官方提供的基準測試資料上效能表現還不錯(雖然資料一大可能就拉胯)。

Hive 的使用介紹這裡就不贅述了,因為真的很簡單直接,在 Hive 裡:

  • Box 就類似 SQL 裡的表

  • Object 就類似於資料庫中的實體物件

  • Adapter 可以用來做自定義物件的介面卡,可以用於實現一些read / write 操作

```dart import 'package:hive/hive.dart';

void main() async { Hive.registerAdapter(PersonAdapter()); var persons = await Hive.openBox('persons');

var person = Person() ..name = 'Lisa';

persons.add(person); // Store this object for the first time

print('Number of persons: ${persons.length}'); print("Lisa's first key: ${person.key}");

person.name = 'Lucas'; person.save(); // Update object

person.delete(); // Remove object from Hive print('Number of persons: ${persons.length}');

persons.put('someKey', person); print("Lisa's second key: ${person.key}"); }

@HiveType() class Person extends HiveObject { @HiveField(0) String name; }

class PersonAdapter extends TypeAdapter { @override final typeId = 0;

@override Person read(BinaryReader reader) { return Person()..name = reader.read(); }

@override void write(BinaryWriter writer, Person obj) { writer.write(obj.name); } } ```

另外 Hive 也支援如 Hive.openLazyBox 進行懶載入和 compactionStrategy 壓縮資料來針對大資料量進行優化,但是正如作者在 #782 介紹的,當資料超過一定數量時,比如 50,000 ,那從效能上還是使用 SQLite 更好。

當然,在 #170 裡有通過對 isolate 提供共享記憶體的支援來優化 Hive ,但是很明顯這條路是走不通的。

另外目前針對 Hive 好像沒有比較合適的視覺化工具,這也許會影響部分人在選型上的考量,不過 hivedb 在 VSCode 上提供了模版程式碼片段的支援,也算是具備另外一種“微弱”的優勢。

PS:其實你可以拿 hivedb 當簡單狀態管理用,並且 Hive 脫離 Flutter 放在 dart server 端也是可以的執行。

同樣支援 key-value 高效儲存的還有 MMKV ,MMKV 同樣是利用 dart:ffi 支援了 Flutter ,相信 Android 開發對它不會陌生, 另外 GetStorage 也是基於二進位制檔案的鍵值對儲存,不過更“低能”一些

isar

也許是因為覺得 Hive 不夠優秀,或者是 Hive 不支援高階查詢,所以作者後續新推了新的資料庫框架: isar ,同樣基於 dart:ffi ,支援 Android 、iOS、Linux、MacOS、Window、Web 平臺。

如果說 Hive 可以簡單代替 shared_preferences ,那 isar 就更像是平替 sqlite 的資料庫,所以 isar 更像是為了解決 query 問題而做的 Hive2.0 ,例如:

  • 支援複合和多索引、查詢修飾符、JSON等
  • 多 isolate 支援
  • 支援十萬條記錄儲存在單個 NoSQL 資料庫

和 Hive 相比 isar 功能更豐富,不再只是單純的 key-value 操作,可以支援更復雜的 filter 等查詢條件,從使用體驗上更符合 NoSQL 的資料庫能力。

```dart await isar.writeTxn(() async { final idsOfUnstarredContacts = await contacts.filter() .isStarredEqualTo(false) .idProperty() .findAll();

contacts.deleteAll(idsOfUnstarredContacts); }); ```

同時 isar 提供了強大的視覺化工具 ,通過 Isar Inspector 也是在一定程度補全了 Hive 的不足,使用 Inspector 只需要在 open 時設定 inspector: true 就可以看到 ws 地址進行繫結訪問。

其實 isar 另外的好處就是完全開源,從構建指令碼到邏輯程式碼都是開源,例如其中通過 Cargo.toml 編排 isar_core_ffi,然後在 github action 會在釋出時通過 shell 指令碼執行如何構建 isar 的動態庫等。

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

例如 isar 在 iOS 上最終接入的是 isar.xcframework ,在 Android 上是 isar.so

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

目前整合 isar 庫本身並不是很大,例如在 Android 上整合之後,大小隻增加了大概 600 多k ,所以作為 Hive 的升級版本,isar 確實值得一試,。

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

目前由於 Isar Web 依賴於 IndexedDB ,所以可能和其他平臺相比較存在一定限制。

最後我們看來自 guide-to-isarflutter-db-benchmarks 的一份資料對比:

  • 左邊的資料對比提供了在 50,000 條資料下
  • 插入耗時 isar < ObjectBox < Realm
  • 刪除耗時 isar < Realm < ObjectBox
  • 資料庫大小 isar < Realm < ObjectBox
  • 條件查詢 isar < Realm < ObjectBox
  • 右邊的資料對比了在 10,000 條資料下
  • 插入耗時 ObjectBox < isarAsync < isarSync < Hive
  • 讀取耗時 Hive < ObjectBox < isarAsync < isarSync
  • 更新耗時 ObjectBox < isarAsync < isarSync < Hive
  • 刪除耗時 ObjectBox < isarAsync < isarSync < Hive

| | | | ----------------------------------------------------------- | ----------------------------------------------------------- |

這兩份資料雖然看起來有矛盾點,但是這其實和測試環境有關係:

  • 左側的圖片時在較為正常裝置上的測試,可以視為一般情況

  • 右側的資料測試的是在低端裝置上的表現,可以視為最壞的情況

就像 #211 裡討論的,同樣的基準測試,作者在它的裝置上測試反而 isar 表現更好,討論的結論上看也是,除了速度之外,在是否導致 UI 卡頓上 isar 的表現也更好,所以基準測試的覆蓋範圍也是一種考量

最後

最後,本篇介紹了 shared_preferencessqlite3DriftrealmObjectBox Hive isar 等資料儲存框架,簡略帶過的還有 sqlcoolfloorMMKVGetStorage 等,可以看到,這份資料也側面體現了目前 Flutter 生態的健全,至少在資料儲存框架上就可以讓人產生“選擇困難症”

如果你還有什麼關於 Flutter 工程或者框架的疑問,歡迎留言評論,這個系列是否更新就取決於是否還有新的素材~