你可能不會 Provider,來一起學一學吧!(分享一個不錯的狀態資料組織方式)

medz發表於2020-07-22

什麼是 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 專案程式設定跨域需求。

相關文章