You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter通用自定义DropdownFormField/DropdownWidget实现求助

通用可复用下拉表单字段实现方案

1. 定义通用选项模型

先统一封装下拉选项的数据结构,适配所有返回id+名称格式的API:

class DropdownOption {
  final String id;
  final String name;

  DropdownOption({required this.id, required this.name});

  // 从API JSON转模型,支持自定义名称字段的key(比如userrole)
  factory DropdownOption.fromJson(Map<String, dynamic> json, {String nameKey = 'name'}) {
    return DropdownOption(
      id: json['id'].toString(),
      name: json[nameKey] as String,
    );
  }
}

2. 通用下拉组件实现

这个组件内置异步加载、状态管理、联动支持,直接复用即可:

import 'package:flutter/material.dart';

class GenericDropdownFormField extends StatefulWidget {
  final String hintText;
  final Future<List<DropdownOption>> Function() fetchOptions;
  final ValueChanged<DropdownOption?> onChanged;
  final DropdownOption? initialValue;
  final bool isEnabled;

  const GenericDropdownFormField({
    super.key,
    required this.hintText,
    required this.fetchOptions,
    required this.onChanged,
    this.initialValue,
    this.isEnabled = true,
  });

  @override
  State<GenericDropdownFormField> createState() => _GenericDropdownFormFieldState();
}

class _GenericDropdownFormFieldState extends State<GenericDropdownFormField> {
  List<DropdownOption> _options = [];
  DropdownOption? _selectedValue;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _selectedValue = widget.initialValue;
    _loadOptions();
  }

  @override
  void didUpdateWidget(covariant GenericDropdownFormField oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 依赖条件变化时(比如州ID变更),自动重新加载选项
    if (oldWidget.fetchOptions != widget.fetchOptions) {
      _loadOptions();
    }
  }

  Future<void> _loadOptions() async {
    if (!widget.isEnabled) return;
    setState(() => _isLoading = true);
    try {
      _options = await widget.fetchOptions();
      // 校验选中值是否在新选项列表中,无效则清空
      if (_selectedValue != null && !_options.any((opt) => opt.id == _selectedValue!.id)) {
        _selectedValue = null;
        widget.onChanged(null);
      }
    } catch (e) {
      debugPrint('下拉选项加载失败: $e');
      // 可根据需求添加错误提示(比如Toast)
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return DropdownButtonFormField<DropdownOption>(
      value: _selectedValue,
      hint: Text(widget.hintText),
      isEnabled: widget.isEnabled && !_isLoading,
      decoration: const InputDecoration(
        border: OutlineInputBorder(),
        contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      ),
      items: _options.map((option) {
        return DropdownMenuItem(
          value: option,
          child: Text(option.name),
        );
      }).toList(),
      onChanged: (value) {
        setState(() => _selectedValue = value);
        widget.onChanged(value);
      },
      disabledHint: _isLoading ? const CircularProgressIndicator(size: 20) : Text(widget.hintText),
    );
  }
}

3. 页面中使用组件(含联动示例)

以下是州、城市、角色下拉框的完整使用示例,API请求部分用模拟数据,实际项目替换为真实接口即可:

import 'package:flutter/material.dart';

class LocationAndRoleForm extends StatefulWidget {
  const LocationAndRoleForm({super.key});

  @override
  State<LocationAndRoleForm> createState() => _LocationAndRoleFormState();
}

class _LocationAndRoleFormState extends State<LocationAndRoleForm> {
  DropdownOption? _selectedState;
  DropdownOption? _selectedCity;
  DropdownOption? _selectedRole;

  // 模拟获取州列表的API请求
  Future<List<DropdownOption>> _fetchStates() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return [
      DropdownOption(id: '1', name: '加利福尼亚州'),
      DropdownOption(id: '2', name: '德克萨斯州'),
      DropdownOption(id: '3', name: '纽约州'),
    ];
  }

  // 模拟根据州ID获取城市列表的API请求
  Future<List<DropdownOption>> _fetchCities() async {
    if (_selectedState == null) return [];
    await Future.delayed(const Duration(milliseconds: 500));
    switch (_selectedState!.id) {
      case '1':
        return [
          DropdownOption(id: '101', name: '洛杉矶'),
          DropdownOption(id: '102', name: '旧金山'),
        ];
      case '2':
        return [
          DropdownOption(id: '201', name: '休斯顿'),
          DropdownOption(id: '202', name: '达拉斯'),
        ];
      case '3':
        return [
          DropdownOption(id: '301', name: '纽约市'),
          DropdownOption(id: '302', name: '布法罗'),
        ];
      default:
        return [];
    }
  }

  // 模拟获取用户角色列表的API请求(注意指定nameKey为userrole)
  Future<List<DropdownOption>> _fetchRoles() async {
    await Future.delayed(const Duration(milliseconds: 500));
    final jsonList = [
      {'id': 'r1', 'userrole': 'president'},
      {'id': 'r2', 'userrole': 'member'},
      {'id': 'r3', 'userrole': 'secretary'},
    ];
    return jsonList.map((json) => DropdownOption.fromJson(json, nameKey: 'userrole')).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('通用下拉组件示例')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            GenericDropdownFormField(
              hintText: '选择州',
              fetchOptions: _fetchStates,
              onChanged: (value) {
                setState(() {
                  _selectedState = value;
                  _selectedCity = null; // 切换州后清空城市选择
                });
              },
            ),
            const SizedBox(height: 16),
            GenericDropdownFormField(
              hintText: '选择城市',
              fetchOptions: _fetchCities,
              onChanged: (value) => setState(() => _selectedCity = value),
              isEnabled: _selectedState != null, // 未选州时禁用
            ),
            const SizedBox(height: 16),
            GenericDropdownFormField(
              hintText: '选择用户角色',
              fetchOptions: _fetchRoles,
              onChanged: (value) => setState(() => _selectedRole = value),
            ),
            const SizedBox(height: 24),
            // 展示选中结果
            Text('选中的州: ${_selectedState?.name ?? '未选择'}'),
            Text('选中的城市: ${_selectedCity?.name ?? '未选择'}'),
            Text('选中的角色: ${_selectedRole?.name ?? '未选择'}'),
          ],
        ),
      ),
    );
  }
}

核心特性说明

  • 适配所有id+名称格式的下拉场景,支持自定义名称字段键
  • 内置加载状态、错误处理,无需重复写逻辑
  • 自动支持联动场景(如州-城市),依赖条件变化时自动刷新选项
  • 支持禁用状态、初始值、提示文字等自定义配置

内容的提问来源于stack exchange,提问作者gaurachand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 03:52:45