欢迎加入开源鸿蒙跨平台社区!https://openharmonycrossplatform.csdn.net

Flutter for OpenHarmony 三级城市选择器的鸿蒙化适配与实现


一、需求背景

在电商、物流、社交等类型的移动应用中,地址选择是一个核心功能模块。用户需要通过"省-市-区"三级联动的方式快速定位到目标区域。一个优秀的城市选择器组件应该具备以下特性:

  1. 交互流畅:滚动选择响应迅速,无卡顿感
  2. 数据准确:行政区划数据及时更新,覆盖全面
  3. 体验友好:支持搜索、历史记录、热门城市等辅助功能
  4. 跨平台一致:在不同操作系统上表现统一

在Flutter for OpenHarmony开发环境中,实现三级城市选择器面临着独特的挑战。由于OpenHarmony的UI组件库与原生Flutter存在差异,特别是CupertinoPicker等iOS风格组件在鸿蒙上的适配需要进行特殊处理。

本文将详细介绍如何在Flutter for OpenHarmony平台下实现一个功能完善的三级城市选择器组件。


二、技术方案设计

2.1 数据结构设计

我们采用嵌套Map结构来存储省市区数据:

final Map<String, Map<String, List<String>>> _cityData = {
  '广东省': {
    '广州市': ['越秀区', '海珠区', '荔湾区', '天河区', '白云区', '黄埔区'],
    '深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区'],
    '东莞市': ['莞城区', '南城区', '东城区', '万江区'],
  },
  '浙江省': {
    '杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区'],
    '宁波市': ['海曙区', '江北区', '北仑区', '鄞州区'],
  },
};

数据结构说明

  • 第一层Key:省份名称
  • 第二层Key:城市名称
  • Value:该城市的区县列表

这种结构的优势在于:

  1. 查找效率高,时间复杂度O(1)
  2. 支持动态添加/删除地区
  3. 便于序列化和持久化存储

2.2 组件架构图

CityPickerDemoPage (主页面)
├── 状态变量
│   ├── _selectedProvince (选中省份)
│   ├── _selectedCity (选中城市)
│   ├── _selectedDistrict (选中区县)
│   └── _selectedAddress (完整地址字符串)
├── 选择方式入口
│   ├── 底部弹窗选择 (_showCityPicker → _CityPickerSheet)
│   └── 快速选择对话框 (_showQuickCityPicker → _QuickCityPickerDialog)
├── 辅助功能
│   ├── 热门城市快捷选择
│   ├── 最近使用记录
│   └── 全部省份列表
└── 选择结果展示
    ├── 地址文本显示
    └── 省/市/区分标签

三、核心功能实现

3.1 底部弹窗选择器

底部弹窗是移动端最常用的选择方式,符合用户的操作习惯:

void _showCityPicker() {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true, // 允许弹窗占满更多空间
    backgroundColor: Colors.transparent, // 自定义背景
    builder: (context) => _CityPickerSheet(
      cityData: _cityData,
      onConfirm: (province, city, district) {
        setState(() {
          _selectedProvince = province;
          _selectedCity = city;
          _selectedDistrict = district;
          _selectedAddress = '$province $city $district';
        });
      },
    ),
  );
}

关键参数解析

  • isScrollControlled: true:让弹窗可以占据屏幕更多高度(默认最大50%)
  • backgroundColor: Colors.transparent:使用自定义圆角背景,而非默认矩形

3.2 三级联动逻辑

三级联动的核心在于:当某一层级的选择发生变化时,自动更新下一层级的数据源。

class _CityPickerSheetState extends State<_CityPickerSheet> {
  late FixedExtentScrollController _provinceController;
  late FixedExtentScrollController _cityController;
  late FixedExtentScrollController _districtController;

  List<String> _provinces = [];
  List<String> _cities = [];
  List<String> _districts = [];

  void _updateCities(int provinceIndex) {
    final province = _provinces[provinceIndex];
    _cities = widget.cityData[province]?.keys.toList() ?? [];
    _updateDistricts(0); // 重置区县为第一个城市的列表
  }

  void _updateDistricts(int cityIndex) {
    if (_cities.isNotEmpty && cityIndex < _cities.length) {
      final city = _cities[cityIndex];
      _districts = widget.cityData[_provinces[_provinceIndex]]?[city] ?? [];
    } else {
      _districts = [];
    }
  }
}

联动流程

  1. 用户滚动省份选择器 → 触发onSelectedItemChanged
  2. 调用_updateCities(provinceIndex)更新城市列表
  3. 城市列表更新后,自动调用_updateDistricts(0)重置区县
  4. 同时重置城市和区县的滚动位置到第一项

3.3 CupertinoPicker在鸿蒙上的适配

CupertinoPicker是Flutter提供的iOS风格滚轮选择器,在OpenHarmony平台上使用时需要注意:

Widget _buildPicker(
  List<String> items,
  FixedExtentScrollController controller,
  Function(int) onChanged,
) {
  return Expanded(
    child: Stack(
      children: [
        // 选中项高亮背景
        Positioned.fill(
          child: Center(
            child: Container(
              height: 40,
              decoration: BoxDecoration(
                color: Colors.cyan.shade50,
                borderRadius: BorderRadius.circular(8),
              ),
            ),
          ),
        ),
        // 滚动选择器
        CupertinoPicker(
          scrollController: controller,
          itemExtent: 40, // 每项高度
          onSelectedItemChanged: onChanged,
          children: items.map((item) {
            return Center(child: Text(item));
          }).toList(),
        ),
      ],
    ),
  );
}

鸿蒙适配要点

  1. 使用Stack叠加选中项背景和选择器本身
  2. itemExtent必须固定,否则会导致布局错乱
  3. 在OpenHarmony上测试发现,magnification属性可能不生效,建议关闭或设置默认值

3.4 弹窗头部操作栏

Widget _buildHeader() {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    decoration: BoxDecoration(
      color: Colors.cyan.shade50,
      borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
    ),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        TextButton(
          onPressed: () => Navigator.pop(context), // 取消
          child: const Text('取消'),
        ),
        const Text('选择城市', style: TextStyle(fontWeight: FontWeight.bold)),
        TextButton(
          onPressed: () {
            // 回调选中的省市区的值
            widget.onConfirm(
              _provinces[_provinceIndex],
              _cities.isNotEmpty ? _cities[_cityIndex] : '',
              _districts.isNotEmpty ? _districts[_districtIndex] : '',
            );
            Navigator.pop(context); // 关闭弹窗
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

四、辅助功能实现

4.1 热门城市快捷选择

对于高频使用的城市,提供一键选择功能可以大幅提升用户体验:

final hotCities = ['北京市', '上海市', '广州市', '深圳市', '杭州市', '南京市'];

Wrap(
  spacing: 10,
  runSpacing: 10,
  children: hotCities.map((city) {
    return InkWell(
      onTap: () {
        final cities = _cityData[city];
        if (cities != null && cities.isNotEmpty) {
          final firstCity = cities.keys.first;
          final districts = cities[firstCity] ?? [];
          setState(() {
            _selectedProvince = city; // 直辖市省份=城市名
            _selectedCity = firstCity;
            _selectedDistrict = districts.first;
          });
        }
      },
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        decoration: BoxDecoration(
          color: Colors.orange.shade50,
          borderRadius: BorderRadius.circular(20),
          border: Border.all(color: Colors.orange.shade200),
        ),
        child: Text(city, style: TextStyle(color: Colors.orange.shade700)),
      ),
    );
  }).toList(),
)

注意:对于北京、上海、天津、重庆四个直辖市,省份和城市名称相同,数据处理时需要特殊判断。

4.2 快速搜索对话框

当用户知道目标省份名称时,通过搜索可以更快地定位:

class _QuickCityPickerDialog extends StatefulWidget {
  // ...
}

class _QuickCityPickerDialogState extends State<_QuickCityPickerDialog> {
  String _searchText = '';
  List<String> _filteredProvinces = [];

  void _filterProvinces(String query) {
    setState(() {
      _searchText = query;
      if (query.isEmpty) {
        _filteredProvinces = widget.cityData.keys.toList();
      } else {
        _filteredProvinces = widget.cityData.keys
            .where((p) => p.toLowerCase().contains(query.toLowerCase()))
            .toList(); // 模糊匹配
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Dialog(
      child: Container(
        width: double.maxFinite,
        height: 500,
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              onChanged: _filterProvinces,
              decoration: InputDecoration(
                hintText: '搜索省份',
                prefixIcon: Icon(Icons.search),
              ),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: _filteredProvinces.length,
                itemBuilder: (context, index) => ListTile(
                  title: Text(_filteredProvinces[index]),
                  onTap: () { /* 选择并回调 */ },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

五、鸿蒙化适配经验总结

5.1 滚动惯性差异

问题现象:在OpenHarmony设备上,CupertinoPicker的滚动惯性比原生iOS更明显,容易出现"滑过头"的情况。

解决方案

CupertinoPicker(
  scrollPhysics: ClampingScrollPhysics(), // 限制过度滚动
  // ...其他参数
)

5.2 弹窗安全区域

问题现象:底部弹窗在有虚拟导航键的鸿蒙设备上,内容会被遮挡。

解决方案

showModalBottomSheet(
  context: context,
  isScrollControlled: true,
  builder: (context) => Padding(
    padding: MediaQuery.of(context).viewInsets, // 处理键盘弹出时的间距
    child: SafeArea( // 处理系统UI(如导航栏)的安全区域
      child: _CityPickerSheet(/* ... */),
    ),
  ),
);

5.3 内存管理

问题现象:频繁打开/关闭城市选择器后,内存占用持续上升。

原因分析:每次打开弹窗都会创建新的_CityPickerSheetState,如果FixedExtentScrollController没有正确释放,会造成内存泄漏。

解决方案


void dispose() {
  _provinceController.dispose();
  _cityController.dispose();
  _districtController.dispose();
  super.dispose();
}

六、运行验证报告

验证项目 测试环境 结果
三级联动正确性 Pineapple (OpenHarmony) ✅ 通过
省份切换后城市更新 Pineapple ✅ 通过
城市切换后区县更新 Pineapple ✅ 通过
热门城市快捷选择 Pineapple ✅ 通过
搜索过滤功能 Pineapple ✅ 通过
最近记录保存 Pineapple ✅ 通过
内存泄漏检测 Pineapple ✅ 无泄漏

性能指标

  • 打开弹窗耗时:< 100ms
  • 滚动帧率:60fps稳定
  • 内存峰值增量:< 8MB
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

七、扩展方向

当前实现的城市选择器还可以从以下方面进行增强:

  1. 定位功能:集成GPS定位,自动选择当前所在城市
  2. 数据热更新:支持从服务器拉取最新的行政区划数据
  3. 拼音搜索:支持输入拼音首字母快速查找城市
  4. 多选模式:支持选择多个城市(如配送范围选择)
  5. 自定义样式:允许开发者自定义颜色主题、字体大小等
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐