Flutter省市区的封装

324 阅读4分钟

前言

我们在使用flutter开发的时候,大多数都会遇到省市区的三级联动。如果整个项目就一个地方用到那就无所谓了,多个地方用到呢?我在想为啥不封装起来(flutter自带组件贼丰富)。这样哪里有需要,哪里就调用可以避免重复写相同的代码。

问题思考

一、封装的话,会用到哪些api呢?

二、在调用封装的组件的时候,需要返回return哪些信息和传递params哪些信息呢?

开始解答

一、封装的话,会用到哪些api呢?

准备阶段

  1. 首先省市区的areaData.json这个需要吧
  2. 既然是三级联动那涉及到滑动选中吧
  3. 得有个取消确定的按钮吧

那好,思考一些会用到哪些Api呢?

  1. 用到areaData.json这个json数据,是不是得弄一个请求,获取json里面的数据目前有两种方案

    1. 通过dio去请求接口数据
    2. areaData.json放到项目assets=>josn=>文件夹下面
  2. 滑动选中的话呢 暂时使用的是CupertinoPicker官方这个组件,详细的api请点击CupertinoPicker说明文档

areaData.json放到assets下需要将对应的路径放到pubspec.yaml如下:

 flutter:
 uses-material-design: true
 # To add assets to your application, add an assets section, like this:
 # assets:
 #   - images/a_dot_burr.jpeg
 #   - images/a_dot_ham.jpeg
 
 assets:
       - assets/json/
 

既然配置了那如何读取数据呢?这个问题我会在代码里注释说明

 import 'package:flutter/services.dart' show rootBundle;
 ​
 // 获取省市区的json数据
 Future _loadCitys() async {
 var cityStr = await rootBundle.loadString('assets/json/areaData.json');
 List areaDatas = json.decode(cityStr) as List; 
 return Future.value(true);
 }

省市区三级联动的封装如下

贴代码

 import 'dart:convert';
 ​
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart' show rootBundle;  
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 ​
 class PickerCitys extends StatefulWidget {
 // 当前选中的省市区的code 数组
 final List? selectList;
 ​
 final Function(List val)? onResult;
 ​
 const PickerCitys({super.key, this.onResult, this.selectList});
 ​
 @override
 State<StatefulWidget> createState() => _PickerCity();
 }
 ​
 class _PickerCity extends State<PickerCitys> {
 List datas = [];
 ​
 int provinceIndex = 0; // 省
 int cityIndex = 0; // 市
 int areaIndex = 0; // 区
 ​
 // 省市区对应的控制器
 late FixedExtentScrollController provinceScrollController;
 late FixedExtentScrollController cityScrollController;
 late FixedExtentScrollController areaScrollController;
 ​
 // 获取省市区的json数据
 Future _loadCitys() async {
 // 此处 可以通过rootBundle 获取本地asset里面的数据
 var cityStr = await rootBundle.loadString('assets/json/areaData.json');
 datas = json.decode(cityStr) as List;
 //result默认取第一组值
 return Future.value(true);
 }
 ​
 // 获取省的数据
 List get provinces {
 if (datas.isNotEmpty) {
 return datas;
 }
 return [];
 }
 ​
 // 获取市的数据
 List get citys {
 if (provinces.isNotEmpty) {
 return provinces[provinceIndex]['children'] ?? [];
 }
 return [];
 }
 ​
 // 获取区的数据
 List get areas {
 if (citys.isNotEmpty) {
 return citys[cityIndex]['children'] ?? [];
 }
 return [];
 }
 ​
 // saveOptions
 List saveOptions() {
 List result = [];
 result = [ {   "option": {"code": provinces[provinceIndex]['value'], "value": provinces[provinceIndex]['text']}
 },
 {
   "option": {"code": citys[cityIndex]['value'], "value": citys[cityIndex]['text']}
 },
 {
   "option": {"code": areas[areaIndex]['value'], "value": areas[areaIndex]['text']}
 }
 ];
 return result;
 }
 ​
 // 通过list-code 获取当前选中的
 ​
 @override
 void initState() {
 // 初始化省市区controller
 provinceScrollController = FixedExtentScrollController();
 cityScrollController = FixedExtentScrollController();
 areaScrollController = FixedExtentScrollController();
 ​
 _loadCitys().then((value) {
 setState(() {
   if (widget.selectList!.isNotEmpty) {
     provinceIndex = provinces.indexWhere((item) => item['value'] == widget.selectList![0]);
     cityIndex = citys.indexWhere((item) => item['value'] == widget.selectList![1]);
     areaIndex = areas.indexWhere((item) => item['value'] == widget.selectList![2]);
   }
   provinceScrollController.jumpToItem(provinceIndex);
   cityScrollController.jumpToItem(cityIndex);
   areaScrollController.jumpToItem(areaIndex);
 });
 });
 ​
 // 初始化数据
 super.initState();
 }
 ​
 @override
 void dispose() {
 provinceScrollController.dispose();
 cityScrollController.dispose();
 areaScrollController.dispose();
 super.dispose();
 }
 ​
 @override
 Widget build(Object context) {
 return Material(
 child: Container(
   width: 375.w,
   height: 300.h,
   decoration: const BoxDecoration(
     color: Colors.white,
   ),
   child: Column(
     mainAxisAlignment: MainAxisAlignment.spaceBetween,
     children: [
       pickerTopBar(),
       pickerBody(),
     ],
   ),
 ),
 );
 }
 ​
 // picker头部
 Widget pickerTopBar() {
 return SizedBox(
 height: 44,
 child: Row(
   mainAxisAlignment: MainAxisAlignment.spaceBetween,
   children: [
     TextButton(
       style: TextButton.styleFrom(
         backgroundColor: Colors.grey[300],
         minimumSize: const Size(60, 44),
         shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
       ),
       onPressed: () {},
       child: Text(
         "取消",
         style: TextStyle(
           fontSize: 16.sp,
           color: Colors.black,
         ),
       ),
     ),
     TextButton(
       style: TextButton.styleFrom(
         backgroundColor: Colors.blue[300],
         minimumSize: const Size(60, 44),
         shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
       ),
       onPressed: () {
         widget.onResult!(saveOptions());
         Navigator.pop(context);
       },
       child: Text(
         "确定",
         style: TextStyle(
           fontSize: 16.sp,
           color: Colors.white,
         ),
       ),
     )
   ],
 ),
 );
 }
 ​
 // picker body
 Widget pickerBody() {
 return Container(
 width: 375.w,
 height: 240.h,
 child: Row(
   children: [
     Expanded(child: provinceView()),
     Expanded(child: cityView()),
     Expanded(child: areaView()),
   ],
 ),
 );
 }
 ​
 Widget provinceView() {
 return Container(
 child: CupertinoPicker(
   scrollController: provinceScrollController,
   itemExtent: 40,
   onSelectedItemChanged: (value) {
     setState(() {
       provinceIndex = value;
     });
 ​
  // 此处是跳到默认的第一项
     if (cityIndex != null) {
       cityIndex = 0;
       if (cityScrollController.positions.isNotEmpty) {
         cityScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
       }
     }
 ​
  if (areaIndex != null) {
       areaIndex = 0;
       if (areaScrollController.positions.isNotEmpty) {
         areaScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
       }
     }
   },
   children: provinces.map((item) {
     return Center(
       child: Text(
         '${item['text']}',
         style: TextStyle(fontSize: 16.sp, color: Colors.black),
         maxLines: 1,
       ),
     );
   }).toList(),
 ),
 );
 }
 ​
 Widget cityView() {
 return Container(
 child: CupertinoPicker(
   scrollController: cityScrollController,
   itemExtent: 40,
   onSelectedItemChanged: (value) {
     setState(() {
       cityIndex = value;
     });
 ​
  if (areaIndex != null) {
       areaIndex = 0;
       if (areaScrollController.positions.isNotEmpty) {
         areaScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
       }
     }
   },
   children: citys.map((item) {
     return Center(
       child: Text(
         '${item['text']}',
         style: TextStyle(fontSize: 16.sp, color: Colors.black),
         maxLines: 1,
       ),
     );
   }).toList(),
 ),
 );
 }
 ​
 Widget areaView() {
 return Container(
 child: CupertinoPicker(
   scrollController: areaScrollController,
   itemExtent: 40,
   onSelectedItemChanged: (value) {
     setState(() {
       areaIndex = value;
     });
   },
   children: areas.map((item) {
     return Center(
       child: Text(
         '${item['text']}',
         style: TextStyle(fontSize: 16.sp, color: Colors.black),
         maxLines: 1,
       ),
     );
   }).toList(),
 ),
 );
 }
 }
 ​

组件中不是使用到了areaData.json么,我这边也将这个json数据贴上来防止迷失了自我(500)

这个areaData.json有点大,我单独开一篇文章去关联这个json

二、在调用封装的组件的时候,需要返回return哪些信息和传递params哪些信息呢?

首先我们需要思考的是为什么要return,为什么要向组件里面传递params

  1. 调用组件的时候return数据出来,是为了将选中的省市区所含有的属性整到一个数组里面这个数组里面有每一项选中的值
  2. 在其他页面调用组件的时候为什么要传递参数呢?首先这个问题同学们应该都知道。重新编辑的时候是需要知道之前选中的是给显示到对应的地方。我习惯性称之为回显 。既然需要显示到页面,如果不传递给组件的话,组件是不知道你选中。
  3. 在进行回显的时候,后端同学一般给的是省市区code,那么我们是不是可以通过code进行回显呢?

组件代码解析

 class PickerCitys extends StatefulWidget {
 ​
 final List? selectList;
 ​
 final Function(List val)? onResult;
 ​
 const PickerCitys({super.key, this.onResult, this.selectList});
 ​
 @override
 State<StatefulWidget> createState() => _PickerCity();
 }

1、其中的selectList 是当前选中的省市区的code 数组,其实也可以是一个字符串。如果是字符串的话还是需要切割或者转化为数组

字符串转数组如下案例

 String selectStr = "1,2,3";
 List<String> selectArr = selectStr.split(',');

2、onResult作为组件调用时的``

 List saveOptions() {
   List result = [];
   result = [     {       "option": {"code": provinces[provinceIndex]['value'], "value": provinces[provinceIndex]['text']}
     },
     {
       "option": {"code": citys[cityIndex]['value'], "value": citys[cityIndex]['text']}
     },
     {
       "option": {"code": areas[areaIndex]['value'], "value": areas[areaIndex]['text']}
     }
   ];
   return result;
 }

其中的result我这里创建的是一个空数组,目的是为了收集选中完后每一项选中的值。

同学们也看到了有这么几个数据provincescitysareas这三组数据实际上对应的就是对应的的数据

其中的provinceIndex表示的是当前选中的关联的数据的索引、以此类推cityIndexareaIndex对应的是的具体数据了。

疑惑的地方

疑惑+1

 @override
 void initState() {
 // 初始化省市区controller
 provinceScrollController = FixedExtentScrollController();
 cityScrollController = FixedExtentScrollController();
 areaScrollController = FixedExtentScrollController();
 ​
 _loadCitys().then((value) {
 setState(() {
   if (widget.selectList!.isNotEmpty) {
     provinceIndex = provinces.indexWhere((item) => item['value'] == widget.selectList![0]);
     cityIndex = citys.indexWhere((item) => item['value'] == widget.selectList![1]);
     areaIndex = areas.indexWhere((item) => item['value'] == widget.selectList![2]);
   }
   provinceScrollController.jumpToItem(provinceIndex);
   cityScrollController.jumpToItem(cityIndex);
   areaScrollController.jumpToItem(areaIndex);
 });
 });
 ​
 // 初始化数据
 super.initState();
 }

在初始化的时候

 provinceScrollController = FixedExtentScrollController(); 

为啥要向上面这样的写呢,不能是这样的么?

 provinceScrollController = FixedExtentScrollController(initialItem: provinceIndex);

按照这样写不是也可以呢,这样不是可以默认就可以选中了么。 同学们这样是可以的,但是有个问题。不知道你们发现了没有

如果在初始化操作直接赋值的话,遇到需要回显的时候,每次打开省市区的选择器的时候是不是都选了第一项去了,这个不是我们的想要的。

我们想要的应该是指哪打哪那种,故才有上面的if判断。通过indexWhere可以知道对应的索引是多少。

数组相关方法=>flutter数组(List)相关方法

 provinceScrollController.jumpToItem(provinceIndex);

其中的jumpToItem可以祝我们一臂之力见FixedExtentScrollController 可以滑动到指定的索引下面

 Widget provinceView() {
 return Container(
 child: CupertinoPicker(
   scrollController: provinceScrollController,
   itemExtent: 40,
   onSelectedItemChanged: (value) {
     setState(() {
       provinceIndex = value;
     });
 ​
  // 此处是跳到默认的第一项
     if (cityIndex != null) {
       cityIndex = 0;
       if (cityScrollController.positions.isNotEmpty) {
         cityScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
       }
     }
 ​
  if (areaIndex != null) {
       areaIndex = 0;
       if (areaScrollController.positions.isNotEmpty) {
         areaScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
       }
     }
   },
   children: provinces.map((item) {
     return Center(
       child: Text(
         '${item['text']}',
         style: TextStyle(fontSize: 16.sp, color: Colors.black),
         maxLines: 1,
       ),
     );
   }).toList(),
 ),
 );
 }
 if (cityIndex != null) {
   cityIndex = 0;
   if (cityScrollController.positions.isNotEmpty) {
     cityScrollController.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
   }
 }

其中的animateTo的目的是为了,当选中不同的的时候,对应的的相关信息也可以显示出来,之所以animateTo(0)是为了从第一个往下选

组件代码调用

调用代码如下:

 Get.bottomSheet(PickerCitys(
   selectList: controller.selectCodes,
   onResult: (val) {
     controller.pcdList.value = val;
   },
 ));

其中使用到Getx中的底部弹框,其中的selectCodes回显的数组数据, onResult是点击确认返回的数据


以上就是省市区的一个封装。 以此为基础的,还可以整出来一个年月日等组件。有疑问的地方请留言!!!,请多支持,不常更新的一个菜鸟


业务时间做的一个小程序,可以探讨全栈(nestjsflutter小程序)相关问题

百科.jpg