这篇文章上次修改于 971 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

团队打算用Flutter 覆写项目,但是遇到了一个有点恼火的事情,以前安卓和IOS在商品的多规格选择一般都有现成的库。Flutter 由于刚兴起,这方面的库我目前还没找到,于是只能自己撸一个了。
首先看看我们某个商品数据结构:

{
  "attributes": [],
  "goods_id": 1636,
  "goods_name": "珍珠奶茶",
  "image": "http://********/static/goods_images/Rlp6d1536542627.png",
  "multi_spec": [
    {
      "image": "http://********/static/goods_images/EQqf01536544612.png",
      "product_code": "",
      "sales_amount": 45,
      "spec_info_list": [
        {
          "spec_name": "规格",
          "spec_name_id": 336,
          "spec_value": "大杯",
          "spec_value_id": 1019
        },
        {
          "spec_name": "颜色",
          "spec_name_id": 337,
          "spec_value": "红色",
          "spec_value_id": 1021
        },
        {
          "spec_name": "重量",
          "spec_name_id": 338,
          "spec_value": "半斤",
          "spec_value_id": 1023
        }
      ],
      "specification_id": 1918,
      "stock": -1,
      "unit": "杯",
      "unit_price": 7
    },
    {
      "image": "http://********/static/goods_images/Fe30S1536544637.png",
      "product_code": "",
      "sales_amount": 16,
      "spec_info_list": [
        {
          "spec_name": "规格",
          "spec_name_id": 336,
          "spec_value": "中杯",
          "spec_value_id": 1020
        },
        {
          "spec_name": "颜色",
          "spec_name_id": 337,
          "spec_value": "绿色",
          "spec_value_id": 1022
        },
        {
          "spec_name": "重量",
          "spec_name_id": 338,
          "spec_value": "一斤",
          "spec_value_id": 1024
        }
      ],
      "specification_id": 1919,
      "stock": -1,
      "unit": "杯",
      "unit_price": 6
    },
    {
      "image": "http://********/static/goods_images/Fe30S1536544637.png",
      "product_code": "",
      "sales_amount": 16,
      "spec_info_list": [
        {
          "spec_name": "规格",
          "spec_name_id": 336,
          "spec_value": "中杯",
          "spec_value_id": 1020
        },
        {
          "spec_name": "颜色",
          "spec_name_id": 337,
          "spec_value": "红色",
          "spec_value_id": 1021
        },
        {
          "spec_name": "重量",
          "spec_name_id": 338,
          "spec_value": "一斤",
          "spec_value_id": 1024
        }
      ],
      "specification_id": 1919,
      "stock": -1,
      "unit": "杯",
      "unit_price": 6
    }
  ],
  "pack_cost": 0,
  "product_code": null,
  "sales_amount": 61,
  "stock": -2,
  "unit": null,
  "unit_price": null
}

multi_spec字段下就是这商品的所有规格搭配,spec_info_list字段下为组成该搭配的各规格值。
我们给分别给规格搭配和组成规格搭配的规格值做了 model :

///商品规格搭配,对应multi_spec的元素
class ShopGoodsMultiSpec {
    String image;
    String unit;
    int specificationId;
    List<ShopGoodsMultiSpecSpecInfo> specInfoList;
    int salesAmount;
    String productCode;
    int stock;
    double unitPrice;
    ///构造函数什么的省略.....
}
///组成规格搭配的规格值,对应spec_info_list的元素
class ShopGoodsMultiSpecSpecInfo {
    int specValueId;
    int specNameId;
    String specName;
    String specValue;
    
    ///重载这个==运算符,为了防止使同一个规格值的两个ShopGoodsMultiSpecSpecInfo对象
    ///被判断为不等,在工具中的 allSpecValue[spec_info.specName].contains(spec_info) 有用,不然会出问题
    // @override
    // int get hashCode => "ShopGoodsMultiSpecSpecInfo_$specValueId".hashCode;
    
    bool operator ==(o){
        ShopGoodsMultiSpecSpecInfo obj = o;
    return obj.specValueId == specValueId;
    }
    
    ///构造函数什么的省略.....
}

贴上工具代码:

class SpecSkuUtil {
  List<String> allSpecKey = [];
  Map<String, ShopGoodsMultiSpec> allSpec = {};
  Map<String, List<ShopGoodsMultiSpecSpecInfo>> allSpecValue = {};
  Map<String, int> selected = {};

  /// 实例化工具,传入所有规格搭配 list
  SpecSkuUtil(List<ShopGoodsMultiSpec> multiSpec) {
    for (ShopGoodsMultiSpec spec in multiSpec) {
      List valueIds = [];
      for (ShopGoodsMultiSpecSpecInfo spec_info in spec.specInfoList) {
        valueIds.add(spec_info.specValueId);
        if (!allSpecValue.containsKey(spec_info.specName)) {
          allSpecValue[spec_info.specName] = [spec_info];
        } else if (!allSpecValue[spec_info.specName].contains(spec_info)) {
          allSpecValue[spec_info.specName].add(spec_info);
        }
      }
      valueIds.sort((a, b) => a.compareTo(b));
      valueIds = valueIds.map((id) {
        return id.toString();
      }).toList();
      allSpec[valueIds.join("-")] = spec;
      _createCollocations(valueIds);
    }
  }

  void _createCollocations(List strList) {
    void build(List candidate, String prefix, int index) {
      if (!allSpecKey.contains(prefix)) {
        allSpecKey.add(prefix);
      }
      for (int i = index; i < candidate.length; i++) {
        List tmp = new List()..addAll(candidate);
        build(tmp, (prefix == "" ? "" : prefix + "-") + tmp.removeAt(i), i);
      }
    }

    build(strList, "", 0);
  }

  /// 返回所有{规格名:List<规格值对象>}
  Map<String, List<ShopGoodsMultiSpecSpecInfo>> getAllSpecValue() {
    return allSpecValue;
  }

  /// 设置已选中的 {规格名:规格值 id}
  void setSelectedIds(Map<String, int> selected) {
    this.selected = selected;
  }

  /// 检查某属性是否可选 {规格名:规格值 id}
  bool checkEnable(Map<String, int> candidate) {
    Map<String, int> tmpMap = Map.from(selected);
    tmpMap.addAll(candidate);
    List tmp = mapValue2List(tmpMap);
    tmp.sort((a, b) => a.compareTo(b));
    return allSpecKey.contains(tmp.join("-"));
  }

  /// 获取规格搭配对象
  ShopGoodsMultiSpec getSpec() {
    List tmp = mapValue2List(selected);
    tmp.sort((a, b) => a.compareTo(b));
    return allSpec[tmp.join("-")];
  }

  /// map的值转 list
  List mapValue2List(Map<String, int> map) {
    List tmp = new List();
    map.forEach((key, value) {
      ///至于判断不为空才加入 list,这个是看实际情况
      ///如果你的程序生成的已选中Map里面不会有 null 就可以不用判断
      if (value != null) {
        tmp.add(value);
      }
    });
    return tmp;
  }
}

核心原理就是,每个规格搭配下的规格值的任意 不重复使用元素 的无序组合 都能代表该规格搭配。
如:

a,b,c  能够生成的组合就是 [a, b, c, ab, ac, bc, abc]

那么,如果我们把每个规格搭配下的规格值的任意不重复使用元素的无序组合都收集在一个list (这里叫它allSpecKey) 中,我们在校验某个规格值是否可选时,只需要把已选择的规格值和待选择的规格值组合起来,然后判断这个组合是否在allSpecKey中就可确定待选择的规格值是否可选。

那么 在渲染每一个规格值按钮组件时,只需要调用checkEnable方法,就可以检查是否可选,getSpec方法可以在选择完一个规格搭配后拿取过个搭配对象。如果没选择完的话,返回的是 null,刚刚方便识别是否有选择完成。当然,每次选中一个规格值时一定要执行setSelectedIds设置一下已经选中的规格Map。