什麼是 Provider?
Provider 是一個 Flutter 框架下的狀態管理工具,不能說最好,但是是優秀狀態管理工具中的佼佼者,再 pub.dev 上 Likes 也是所有狀態管理工具的第一名,GitHub Star 也其高。括號:先對 Dart 或者 Flutter 包而言,因為 Dart packages 和 Flutter packages 的Star 數量普遍不高,畢竟入門門檻還是有點高。括號括轉!
什麼是狀態管理?
Emmm,咋個解釋呢,用 Vue 的直直到 Vuex 撒?用React 的直到 redux 撒?或者和 Mobx。就這種工具咯
先了解下資料
寫客戶端常與 APIs 打交道,如果做過後端的,其實就是將資料庫的資料進行封裝再輸出到請求的 response body 裡面。
那麼,我們完全可以逆向思維,我們 APIs 返回的資料進行拆分重新組裝成集合,是不是方便管理資料了呢?
為什麼要逆向拆分還原會再伺服器資料庫的大致結構狀態?
舉個例子,一個帖子介面還會返回發帖人的使用者資訊。你存在一起作為帖子狀態類成員。如果全域性管理一個狀態呢?比如你關注了這個使用者,你不需要再去拉新的帖子資料和使用者資料,直接修改這個使用者再本地的關注狀態即可。
服務端給的資料結構:
{
"title": "帖子",
"user": {
"name": "使用者名稱"
}
}
拆分後:
{"title": "Post Title"}
{"name": "user name"}
當然,用 Json 去才分真的是難受。我個人再用 built_value 這個工具,先將資料 Model 化然後再拆分的
資料集合
如上資料,我們需要兩個資料狀態集合,一個叫 Users
一個叫 Posts
基類
為什麼要寫資料基類?我之前也不寫,集合寫多了後發現每個集合暴露的方法都不一樣。開發和維護特別難受,邊寫基類的好好處就是將集合所暴露的 API 統一起來。
分享我邊寫的基類:
import 'package:flutter/foundation.dart';
/// 使用者集合處理結果 keys 值列舉
///
/// 專門用於處理原始集合後返回處理結果
enum CollectionProviderActions {
/// 插入的資料集合
inserts,
/// 更新的資料集合
updates,
}
/// 資料集合基類,專門用於處理資料集合中通用部分
abstract class BaseCollectionProvider<K, V> with ChangeNotifier {
/// 儲存資料集合原始資料資訊
/// 以鍵值對形式儲存私有的 [_collections] 中,型別為 `Map<K, V>` 形式儲存
Map<K, V> _collections = {};
/// 只讀形式獲取 [_collections] 資訊
Map<K, V> get collections => _collections;
/// 獲取一個 [String] 型別的集合名稱,主要用於 [watcher] 進行使用.
///
/// 使用的時候需要再子類中定義 [collectionName] 的 getter.
String get collectionName;
/// Returns true if this map contains the given [key].
///
/// Returns true if any of the keys in the map are equal to `key`
/// according to the equality used by the map.
bool containsKey(K key) => collections.containsKey(key);
/// Returns true if this map contains the given [value].
bool containsValue(V value) => collections.containsValue(value);
/// 獲取當前物件的 `key` 集合
Iterable<K> get keys => collections.keys;
/// 獲取當前物件的 `value` 集合
Iterable<V> get values => collections.values;
/// 原始好資訊處理,處理完成後將返回以 `Map<CollectionProviderActions, Iterable<V>>`
/// 為基準的原始資訊。
///
/// 其中 [CollectionProviderActions.inserts] 用於記錄插入多少新文件;
/// 而 [CollectionProviderActions.updates] 用於記錄更新了多少文件;
///
/// 返回資訊如下:
/// ```dart
/// {
/// CollectionProviderActions.inserts: <V>[...],
/// CollectionProviderActions.updates: <V>[...],
/// }
/// ```
///
/// 注意,傳入的引數 [elements] 可以是 `Iterable<V>` 和 `Iterable<Object>`
/// 或者 `Iterable<dynamic>`;程式判斷出是 [V] 型別資料則直接跳過,其他情況均
/// 呼叫 [formObject] 函式進行資料處理。
Map<CollectionProviderActions, Iterable<V>> originInsertOrUpdate(
Iterable<Object> elements) {
/// 如果文件集合不存在,則直接返回空資訊
if (elements == null || elements.isEmpty) {
return {
CollectionProviderActions.inserts: [],
CollectionProviderActions.updates: [],
};
}
/// 將 [elements] 轉化為 `Iterable<V>` 資料。
Iterable<V> objects =
elements.where((element) => element is! V).map<V>(formObject).toList()
..addAll(elements.whereType<V>())
..removeWhere((element) => element == null);
/// 獲取需要插入的資料集合
final Map<K, V> inserts = objects
.where((element) => !collections.containsKey(toCollectionId(element)))
.toList()
.asMap()
.map<K, V>(
(_, V element) => MapEntry(toCollectionId(element), element));
/// 獲取需要更新的資料集合
final Map<K, V> updates = objects
.where((V element) => !inserts.containsKey(toCollectionId(element)))
.toList()
.asMap()
.map<K, V>(
(_, V element) => MapEntry(toCollectionId(element), element));
/// 將更新的資料集合放入 [collections] 中
_collections.updateAll((key, value) {
if (updates.containsKey(key)) {
return updates[key];
}
return value;
});
/// 將需要插入的資料插入到 [collections] 中。
_collections.addAll(inserts);
/// 返回處理皇后的元資訊
return {
CollectionProviderActions.inserts: inserts.values,
CollectionProviderActions.updates: updates.values,
};
}
/// 插入或者更新指定的資料集合,其方法參考 [originInsertOrUpdate] 函
/// 數,其區別在於 [insertOrUpdate] 不返回任何結果資訊,並且給插入的數
/// 據集合設定一個 [CloudBase] 的資料 [watcher] 來保證服務端資料更改。
Map<CollectionProviderActions, Iterable<V>> insertOrUpdate(
Iterable<Object> elements) {
final Map<CollectionProviderActions, Iterable<V>> state =
originInsertOrUpdate(elements);
notifyListeners();
Iterable<V> inserts = state[CollectionProviderActions.inserts];
if (inserts != null && inserts.isNotEmpty) {
watcher(inserts);
}
return state;
}
/// 轉換 [element] 為資料監聽者所需的 ID
String toDocId(V element);
/// 轉換 [element] 為集合儲存所需要的 [K] 值
K toCollectionId(V element);
@mustCallSuper
V formObject(Object value) {
if (value is V) {
return value;
}
return null;
}
/// 便捷從集合中使用 [key] 獲取文件
V operator [](K key) => collections[key];
/// 用於便捷的設定單個文件的更新
void operator []=(K key, V value) => insertOrUpdate([value]);
}
集合子類
import 'package:app/models/user.dart';
import 'package:app/provider/collection.dart';
class UsersCollection extends BaseCollectionProvider<String, User> {
static UsersCollection _instance;
factory UsersCollection() {
if (_instance == null) {
_instance = UsersCollection._();
}
return _instance;
}
UsersCollection._();
@override
User formObject(Object value) {
return super.formObject(value) ?? User.fromJson(value);
}
@override
String toCollectionId(User value) => value.id;
@override
String toDocId(User value) => toCollectionId(value);
@override
String get collectionName => "users";
}
基本上常用的資料集合常用方法都有了,沒有的可以直接獲取 collections
這個 Map 進行操作
使用
我們在 MultiProvider
中加入集合 provider :
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => MomentsCollection()),
ChangeNotifierProvider(create: (_) => UsersCollection()),
],
child: child,
);
}
然後在 Widget 裡面就可以使用 context.read
/context/watch
/context.select
方法進行使用咯。
仔細看子類
沒錯,子類增加了單例工廠函式!其原因很簡單,我們總有一些地方沒有 BuildContext
吧?增加單例就可以直接使用這個類進行資料更新。簡直不要太方便。
沒了
看啥呢?還不快起寫程式碼?我就是很久沒寫文章了抽個十來分鐘分享一些做了一年多 Flutter 開發的部分經驗
GitHub
還在看?那去Follow一些我的GitHub吧! github.com/medz
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Seven 的程式碼太渣,歡迎關注我的新擴充包 medz/cors 解決 PHP 專案程式設定跨域需求。