如何设计一个通用且好用的筛选组件

413 阅读7分钟

在业务需求开发中,筛选是比较常用的功能,在业务开发的列表页面中经常出现,是最基础的筛选过滤方式。筛选组件逻辑复杂,UI 交互形式多样,但功能单一,通用性和扩展性都比较差,没办法满足各个业务的需求。

那么,你有没有想过改进方案呢?下面我来说说贝壳团队针对筛选组件问题的解决方案,希望能给你带来一点启发。

我们先来看一组图,从实例中切实体会一下筛选页面的复杂程度:

可以看出,筛选有列表、标签、区间、单选、多选等单一样式展示,又有混合样式展示,而且存在一级、二级、三级筛选,标签间互斥,还有要求限制选择个数和默认选项等交互逻辑。

面对样式众多、交互复杂不一致的情况,接下来,我们从规范数据协议、统一筛选交互、历史问题适配三个方面来解决筛选组件问题,实现能兼容已有能力并提供未来可能的拓展。

规范数据协议

先来讲如何规范数据协议,首先我们对比一些贝壳常用的筛选数据的协议:

我们可以看出:

  • 业务 A 的数据格式虽然满足当前业务需求,但筛选数据格式较简单,不支持多选、自定义范围、时间范围的筛选类型,而且筛选数据和业务数据在同一个接口返回,筛选与业务耦合。
  • 业务 B 中实现筛选功能需要理解筛选数据的字段含义,如字段“price”和具体的筛选项绑定,当字段变动、增加时需要重新开发,后期维护成本增加。

那么什么样的数据协议可以既适用 A 又适用 B 呢?我们通过样式和交互抽象出了一套通用的数据协议:

这一套数据字段设计实现了筛选数据标准化、一般化,把具体业务字段约束在通用字段中,数据字段对 UI 实现透明,不再需要理解筛选字段中包含的业务信息,同时解决了业务 A、B 在字段设计上存在的支持功能不全的问题。

下面是该数据协议的代码实现部分:

class SelectionEntity {
  /// 唯一标识
  String uniqueId;
  /// 筛选显示的文案
  String name;
  /// 筛选数据类型是单选、多选、自定义输入等类型
  String type;
  /// 回传给服务器的 key
  String key;
  /// 回传给服务器的 value
  String value;
  /// 默认选中值,若 defaultValue 与 children 字段中
  /// 某一个子筛选项的 value 值相同,则在数据初始化时将 改子筛选项设置为选中状态
  String defaultValue;
  /// 子筛选项
  List<BrnSelectionEntity> children; //下级筛选项
  /// 扩展字段,目前用于range、dateRange类型的筛选项中
  /// 用于限定值类型范围
  /// ext 中有三个字段,min 和 max 标识最大值、最小值范围
  /// unit单位,例如居室、万,适配自定义区间填写内容
  Map extMap;
}

为了让你更直观地理解该筛选数据协议设计思路,我们来看下它的结构示意图:

在这个筛选数据协议中,我们先看一级筛选,筛选菜单(如“租金”)内部通过 children 字段持有子筛选项列表,构成了父子层级关系、递归实现层级关系。当用户点击“2000-2500 元”的子筛选项时,筛选组件将从父级获取 key 字段的值(rent_price),并从子级获取 value 字段的值(2000:2500),返回给外部格式如下:

{
      rent_price: 2000:2500
}

下面看下协议中的二级筛选。

其实原理同上面的一级筛选是一样,如果一级子筛选项内部嵌套了二级子筛选项,那么当我们选中二级子筛选项时,key 也会从它的上级也就是一级子筛选中获取,与之对应的 value 将从选中的二级子筛选项获取。以下为示意图:

当点击【租金】下【月租金】且选择【月租金】下【2000-2500 元】选项时,对应的数据结构如右图 [key1] 对应取 [value1] 的值,[key2] 对应取 [value2] 的值,均取其子选项值,最终得到的数据格式如下:

   {
      rent_price: month_rent,
      month_price:2000:2500
   }

上面我的举例都是单选(radio)类型筛选,这个筛选数据协议还支持多选(checkbox)类型、值范围(range)类型、日期范围(daterange)类型等其它类型的筛选。你只要根据 type 字段判断不同类型对应的不同交互逻辑就可以了,其数据结构相同,这里不再赘述。

如何规范数据协议部分讲完了,我们来简单总结筛选协议回传数据规则:在筛选组件中,当前选中的筛选项的 value 与它的父级筛选项的 key 配对返回,如果遇到筛选项嵌套的情况,则依次按照这个规则处理选中的筛选参数,一起返回。

统一筛选交互

在规范数据协议之后,接着我们看一下如何统一交互规范,这里还是以贝壳为例。

交互的源头毫无疑问是设计,因此设计团队在兼容众多业务筛选功能的情况下,规范了筛选交互如图:

图中可以看出支持筛选类型包括多选、单选、值范围、时间范围等,展示样式包括列表样式、Tag 样式等,功能十分丰富。并且这套规范已落入贝壳移动端标准组件库中,目前贝壳 70% 业务方都已接入标准库,使用建设完成的通用筛选组件。

历史问题适配

讲到这里,相信你已经理解了统一的筛选协议能带来的好处。后端返回的筛选数据对业务是透明的,使用时,我们不需要理解筛选数据的具体含义。

但是想要建立这样一套统一协议就没有缺点吗?当然也是有的。

因为在业务初期为快速迭代,各团队都会建设自己的一套筛选协议,已有服务无法根据我们新建立的筛选数据结构下发数据,这时该怎么办呢?

这个时候,变动已有的服务往往是牵一发而动全身,成本太高。其实, 针对各业务已有筛选协议,我们可以提供一个数据转换器。只需要在使用筛选组件前,用数据转换器把原本数据协议转换为符合上述协议的数据,再传给筛选组件就可以了。

有了转换器,数据协议的差异就不再是瓶颈。

我们来看一看,如何把不标准的筛选数据结构,转换为上文提到的 SelectionEntity 标准数据结构。

数据转换的代码根据实际情况会有所变动,在这里我简单举一个适配已有数据协议的样例代码,给你做参考。

void _initData() {
    items.clear();
    for (Map enumNameMap in enumNameArr) {
      if (enumNameMap['key'] == 'progressStatus') {
        enumNameMap['dataArray'] = model.progressStatusArray != null &&
                model.progressStatusArray.length > 0
            ? model.progressStatusArray
            : [];
      } else if (enumNameMap['key'] == 'threadSources') {
        enumNameMap['dataArray'] = model.threadSourcesArray != null &&
                model.threadSourcesArray.length > 0
            ? model.threadSourcesArray
            : [];
      }
      SelectionEntity entity = new SelectionEntity(
        name: enumNameMap['name'],
        type: enumNameMap['type'],
        key: enumNameMap['key'],
        children: getEnumChildren(enumNameMap['dataArray']),
        uniqueId: enumNameMap['uniqueId'],
      );
      items.add(entity);
    }
  }

以上三点就是我们团队针对筛选组件问题的解决方案,这个方案自上线以来,获得了各业务线同学的认可,取得了很好的提效效果。

总结

今天的讲解到这里就结束了。我简单的梳理下我们今天讲解的内容,你可以对照思维导图来复习。

为了解决筛选与业务耦合的问题,我们从实际业务场景出发,通过抽象交互逻辑得到一套可以满足所有业务场景的筛选组件,在实际操作层面体现为三方面:规范数据协议、统一筛选交互和历史问题适配。最终实现让业务解耦、提高复用率、节约维护成本、减少接入成本。

数据协议是决定筛选组件是否真正好用易用的关键