需求简介
涉及到地址相关的内容,首先要区分国内和国外。如果涉外,第一层就是选国家。如果仅仅是国内,那么会分成两部分:
省市区三级(比如浙江省,杭州市,临安区)一般采用选择的方式;
详细地址(街道,社区,门牌号),一般会提供一个输入框进行填写;
插件选择
- 省市区三级选择,我们采用了flutter_city_picker这个插件。虽然热度不是很高,不过用下来感觉还可以的。
- 按首字母进行分组,这里就要用到插件lpinyin。虽然热度也不是很高,但是实用。
数据处理
接口获取31个省市区三级数据
需要后台提供一个接口,把全国31个省市区的内容都返回。比如我们这里就用map嵌套的方式返回31个省市区的数据。
将Map转化为插件需要的数据结构
- 插件需要的数据结构AddressNode定义如下:
/// 城市的数据模型
class AddressNode {
/// 名称
String? name;
/// 代码
String? code;
/// 首字母
String? letter;
AddressNode({
this.name,
this.code,
this.letter,
});
factory AddressNode.fromJson(Map<String, dynamic> json) {
return AddressNode(
name: json["name"].toString(),
code: json["code"].toString(),
letter: json["letter"].toString(),
);
}
}
- 为了满足插件的需要,定义了如下变量,保留中间的交互数据
/// 选择后显示的文本,当前的格式为“省市区”,name字段直接拼接
String area = "";
/// 从后台获取所有省市区三级数据,Map结构
List allCityList = [];
/// 用户选择的省,有带选择的城市列表,Map结构
Map selectProvince = {};
/// 用户选择的城市,有带选择的区列表,Map结构
Map selectCity = {};
/// 插件需要的省列表,AddressNode结构
List<AddressNode> firstList = [];
/// 插件需要的城市列表,AddressNode结构
List<AddressNode> secondList = [];
/// 插件需要的地区列表,AddressNode结构
List<AddressNode> thirdList = [];
/// 省市区三级结构选择完毕后,选择组件消失,返回用户选择的省市区三个AddressNode结构
List<AddressNode> finishData = [];
- 获取省市区三级数据的后台调用,返回的是三级的Map列表。由于第一级的插件数据List<AddressNode> firstList固定,所以,这个时候可以一起确定。一进入页面就可以调用接口把数据获取到,等用户选择地区的时候直接显示。
/// 获取所有省市区三级数据的接口调用
/// 同时构造选择组件的第1级数据(省列表)
void getCityList() async {
ApiResponse response = await CommonApi.getCities();
if (response.code == 0) {
allCityList = response.data;
firstList = allCityList.map((item) => AddressNode(
name: item['name'],
code: item['code'],
letter: PinyinHelper.getFirstWordPinyin(item['name']).substring(0, 1).toUpperCase(),
)).toList();
update();
}
}
实现接口
插件的城市列表和地区列表数据的构造方式比较特殊,需要实现接口CityPickerListener中定义的方法。由于我们采用的是GetX构造页面,所以我们可以在Logic文件中实现接口函数。
class EditProfileLogic extends GetxController implements CityPickerListener {
/// 其他代码……
@override
Future<List<AddressNode>> onDataLoad(int index, String code, String name) async {
if (index == 0) {
return firstList;
} else if (index == 1) {
for (Map province in allCityList) {
if (province["code"] == code && province["name"] == name ) {
selectProvince = province;
break;
}
}
List cityList = selectProvince["children"] ?? [];
secondList = cityList.map((item) => AddressNode(
name: item['name'],
code: item['code'],
letter: PinyinHelper.getFirstWordPinyin(item['name']).substring(0, 1).toUpperCase(),
)).toList();
return secondList;
} else if (index == 2) {
List cityList = selectProvince["children"] ?? [];
for (Map city in cityList) {
if (city["code"] == code && city["name"] == name ) {
selectCity = city;
break;
}
}
List districtList = selectCity["children"] ?? [];
thirdList = districtList.map((item) => AddressNode(
name: item['name'],
code: item['code'],
letter: PinyinHelper.getFirstWordPinyin(item['name']).substring(0, 1).toUpperCase(),
)).toList();
return thirdList;
} else {
return [];
}
}
}
第1列(省列表)的逻辑比较简单,在调用后台接口时候构造一次就可以了,这里直接调用就好。
第2列(城市列表)的逻辑稍微复杂一点:onDataLoad中的参数name和code是用户选中的省的name和code;通过这两个参数,就可以遍历原始的allCityList,找出选中的省,数据暂存在selectProvince这个Map变量中。其中的children字段就是该省对应的城市列表,将Map数据成员转化为AddressNode成员(会丢失children字段),就是第2列的显示数据,保存在secondList中),返回给插件使用。
第3列(地区列表)的逻辑和城市列表的逻辑类似:onDataLoad中的参数name和code是用户选中的城市的name和code;而遍历的对象就是selectProvince这个Map变量的children字段,这样就可以把用户选择的城市找出来,暂存在selectCity中,其中的children字段就是该城市对应的地区列表。将数据成员由Map转化为AddressNode,就是第3列的显示数据,保存在thirdList,返回给插件使用。
省市区三级结构选择完成后的处理
这个可以再接口函数onFinish中处理。我们这里的处理代码如下所示:
@override
void onFinish(List<AddressNode> data) async {
/// 拼接用户选择的字符串
String newCity = "";
for (var node in data) {
newCity += "${node.name}";
}
/// 调用接口修改数据
ApiResponse response =
await PersonApi.editDetail({"city": newCity});
if (response.code == 0) {
/// 更新显示,并保留用户上次选择的数据
area = newCity;
finishData = data;
update();
/// 发送消息,让我的页面更新用户信息
EventBusExtension.emitUpdateUserInfo({"source": "编辑资料页面修改地区"});
} else {
ToastUtil.showText(text: response.msg ?? "");
}
}
接口函数onFinish的参数List<AddressNode> data就是用户选择的省市区三个AddressNode结构。
我们根据返回的用户选择数据,构造接口需要的数据;然后调用接口,修改后台数据。接口成功之后,回显在界面上,同时将用户选择的数据保存起来。
如果接口调用失败,那么相当于什么也做。
用户选择的数据保存在finishData变量当中,作为参数,传入插件的界面组件中,可以在第二次显示插件的时候以不同的颜色显示上次的选项。
界面组件
界面组件包装成了一个静态函数,用起来比较方便。这里是一个点击响应,点一下,显示地址选择插件。在GetX的view文件中。
onSelectClick: () {
CityPicker.show(
context: Get.context!,
cityPickerListener: logic, /// 实现接口的类,这里是GetX的Logic
initialAddress: logic.finishData, /// 用户上次的选项,在第二次显示的时候会以不同颜色显示
itemHeadLineColor: StyleUtils.themeColor,
selectedLabelColor: StyleUtils.themeColor,
tabIndicatorColor: StyleUtils.themeColor,
itemSelectedTextStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: StyleUtils.themeColor),
);
},
扩展
不单单是城市选择,其实其他的多级选择也可以用这个插件(比如标签)。数据元素name和code两个字段一个用来显示,一个用来做id,很多场景也是够用了。
级数也不一定是三级,可以根据实际情况进行定义。多几级少几级都是可以的,只要在接口函数中定义清楚就可以了。