你可能不会 Provider,来一起学一学吧!(分享一个不错的状态数据组织方式)

什么是 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 项目程序设置跨域需求。
讨论数量: 1
medz

从服务器拿到数据后直接扔进集合,结果集中有数据可直接用,或者用的地方直接从状态集合中查询数据即可。

2个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!
创始人 @ Byte Gem
文章
33
粉丝
190
喜欢
518
收藏
163
排名:22
访问:12.2 万
私信
所有博文