Flutter横向树形选择器实现方案

撸了个横向树形选择器,分享一下踩坑记录

最近项目里需要做一个多级分类选择,类似淘宝那种"电子产品 > 手机 > iPhone"的效果。找了一圈没看到合适的轮子,索性自己撸一个。

先看效果

大概就是这个样子,点击一级会展开下一级,顶部有面包屑导航可以回退,支持搜索。

为什么要横向?

说实话一开始我是想用纵向折叠的方案的,但产品觉得层级多了以后太丑了,而且用户不知道自己选到第几层。后来看了一些竞品,发现横向展开+面包屑这个方案确实清晰很多,用户一眼就知道自己选了什么。

核心思路

其实整个组件的核心就是维护两个状态:

  • _breadcrumbs - 面包屑路径,记录用户点了哪些父级
  • _currentOptions - 当前显示的选项列表

每次点击一个有子项的选项,就把它加到面包屑里,然后把它的 children 设成当前选项列表。点面包屑就反向操作,截取数组就行。

dart 复制代码
/// 导航到子项
void _navigateToChildren(T parent) {
  setState(() {
    _breadcrumbs.add(parent);
    _currentOptions = parent.children?.map((child) => child as T).toList() ?? [];
  });
}

/// 导航到面包屑指定位置
void _navigateToBreadcrumb(int index) {
  setState(() {
    _breadcrumbs = _breadcrumbs.sublist(0, index + 1);
    _currentOptions = _breadcrumbs[index].children?.map((child) => child as T).toList() ?? [];
  });
}

回显这个坑

最麻烦的其实是回显,就是用户之前选过的值要显示出来。这就需要在整棵树里递归查找,找到后把完整路径构建出来:

dart 复制代码
/// 递归查找项目路径
bool _findItemPath(List<T> options, T target, List<T> path) {
  for (var option in options) {
    path.add(option);

    if (option.value == target.value) {
      return true;
    }

    if (option.children != null && option.children!.isNotEmpty) {
      List<T> children = option.children!.map((child) => child as T).toList();
      if (_findItemPath(children, target, path)) {
        return true;
      }
    }

    path.removeLast();  // 回溯
  }
  return false;
}

这个回溯算法挺经典的,找到了就一路返回 true,没找到就把刚加的节点删掉继续找下一个。

搜索功能

搜索功能也是后来加的,需求是只搜叶子节点(最终可选的项),然后显示完整路径让用户知道这个选项在哪。

dart 复制代码
void _searchInTree(List<T> options, String query, List<T> results, List<T> path) {
  for (var option in options) {
    final bool isLeaf = option.children == null || option.children!.isEmpty;
    final bool nameMatches = option.name?.toLowerCase().contains(query) == true;
    
    if (isLeaf && nameMatches) {
      results.add(option);
    }

    // 递归搜索子项
    if (option.children != null && option.children!.isNotEmpty) {
      List<T> newPath = List.from(path)..add(option);
      List<T> children = option.children!.map((child) => child as T).toList();
      _searchInTree(children, query, results, newPath);
    }
  }
}

数据结构设计

为了通用性,我定义了一个抽象类,业务方继承一下就能用:

dart 复制代码
abstract class OptionChildrenItem {
  String? name;
  String? value;
  List<OptionChildrenItem>? children;

  OptionChildrenItem({this.name, this.value, this.children});
}

也提供了一个默认实现 OptionChildrenItemImpl,不想自定义可以直接用。

使用方式

用起来很简单,调一个方法就行:

dart 复制代码
final result = await showHorizontalTreePopup(
  context: context,
  options: _demoOptions,
  title: '请选择分类',
  themeColor: Colors.blue,
  showSearch: true,  // 要不要搜索框
  showConfirmButton: true,  // 要不要确定按钮,false 的话选中直接返回
  onConfirm: (selectedItem) {
    print('选中了: ${selectedItem?.name}');
  },
);

一些细节处理

  1. 底部安全区 - iPhone 那个小横条要留出来,用 MediaQuery.of(context).padding.bottom

  2. 泛型支持 - 整个组件是泛型的,T extends OptionChildrenItem,这样业务方自己的数据模型也能用

  3. 两种模式 - showConfirmButton 为 true 时需要点确定才返回,为 false 时点击叶子节点直接返回,适合不同场景

总结

整体下来代码量不大,700 多行,核心逻辑就是维护面包屑和当前列表这两个状态。最坑的地方是泛型转换,Dart 的泛型协变有点坑,children 那里需要手动 map 转一下类型。

有需要的可以直接用,pub.dev 搜 horizontal_tree_selector 就行:

yaml 复制代码
dependencies:
  horizontal_tree_selector: ^0.0.1

代码写得比较糙,有问题欢迎提 issue~

相关推荐
Kapaseker4 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton7 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
chinesegf7 小时前
iTunes Lookup API 规则具体(查包名)
ios
ji_shuke7 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday04269 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理11 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台11 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐11 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极11 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan11 小时前
setHintTextColor不生效
android