4、日期与时间选择器:DatePicker与TimePicker在表单中的集成

日期和时间选择,几乎是每个表单都绕不开的坎。我记得刚做Flutter那会儿,有个用户注册的页面,用户要填出生日期和预约时间。我直接用了个TextField让用户手打,结果上线第一天就被骂惨了——格式五花八门,有人写“2024-3-5”,有人写“24年3月5号”,还有人写“三月五号”。嗯,从那以后,我再也不敢让用户手动输入日期了。

说白了,日期时间选择器就是帮用户“选”而不是“填”的组件。Flutter原生提供了showDatePickershowTimePicker,用起来很方便。但真正集成到表单里,还是有不少细节要注意的。今天我就把实战中积累的经验分享给你。

4.1 基础用法:DatePicker和TimePicker

先看最基础的调用方式。我个人习惯把日期选择封装成一个异步方法,这样调用起来更干净。

Future<DateTime?> pickDate(BuildContext context) async {
  final picked = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime(1900),
    lastDate: DateTime(2100),
  );
  return picked;
}

这里有几个参数要注意:

  • initialDate:打开时默认选中的日期。我一般传当前日期,或者表单里已有的值。
  • firstDate / lastDate:可选范围。比如出生日期,我通常限制在1900年到今天。
  • context:必须传,因为它是弹窗式的。

时间选择器类似:

Future<TimeOfDay?> pickTime(BuildContext context) async {
  final picked = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
  );
  return picked;
}

你想想看,这两个API返回的都是Future,所以用await等着就行。选完返回null?那就是用户取消了。

4.2 在表单中集成:状态管理是关键

表单里用日期选择器,最核心的问题就是:选完的值怎么存?怎么展示?

我建议用TextEditingController配合TextFormField,但注意——TextFormField只是用来展示格式化后的字符串,真正的日期值要单独存。

核心思路:日期值用DateTime?变量存,展示用TextEditingController格式化显示。两者分开管理。

来看一个完整的例子:

class DateFieldWidget extends StatefulWidget {
  @override
  _DateFieldWidgetState createState() => _DateFieldWidgetState();
}

class _DateFieldWidgetState extends State<DateFieldWidget> {
  final _dateController = TextEditingController();
  DateTime? _selectedDate;

  Future<void> _selectDate() async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _selectedDate ?? DateTime.now(),
      firstDate: DateTime(1900),
      lastDate: DateTime(2100),
    );
    if (picked != null) {
      setState(() {
        _selectedDate = picked;
        _dateController.text = 
            '${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: _dateController,
      readOnly: true, // 禁止键盘输入
      decoration: InputDecoration(
        labelText: '出生日期',
        suffixIcon: Icon(Icons.calendar_today),
      ),
      onTap: _selectDate,
      validator: (value) {
        if (_selectedDate == null) return '请选择日期';
        return null;
      },
    );
  }

  @override
  void dispose() {
    _dateController.dispose();
    super.dispose();
  }
}

这里有个小技巧:readOnly: true加上onTap,用户点击输入框就会弹出日期选择器,而不是弹出键盘。用户体验好很多。

提示:日期格式化建议用intl包的DateFormat类,更灵活。比如DateFormat('yyyy-MM-dd').format(picked),还能处理国际化。

4.3 时间选择器的集成

时间选择器的集成思路完全一样。不过要注意,showTimePicker返回的是TimeOfDay,不是DateTime。如果你需要完整的日期时间,得自己组合。

Future<void> _selectTime() async {
  final picked = await showTimePicker(
    context: context,
    initialTime: _selectedTime ?? TimeOfDay.now(),
  );
  if (picked != null) {
    setState(() {
      _selectedTime = picked;
      _timeController.text = picked.format(context);
    });
  }
}

嗯,这里要注意:TimeOfDay.format(context)会根据系统语言返回12小时制或24小时制。如果你需要固定格式,可以自己拼字符串:

final timeStr = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';

4.4 组合日期+时间:一个字段搞定

有些场景需要同时选日期和时间,比如预约会议。Flutter没有直接提供组合选择器,但我们可以自己拼。

我的做法是:先选日期,再选时间,然后合并成一个DateTime

Future<DateTime?> _selectDateTime(BuildContext context) async {
  final date = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime.now(),
    lastDate: DateTime.now().add(Duration(days: 365)),
  );
  if (date == null) return null;

  final time = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.fromDateTime(date),
  );
  if (time == null) return null;

  return DateTime(date.year, date.month, date.day, time.hour, time.minute);
}

你想想看,这样用户先选日期,再选时间,流程很自然。而且我限制了firstDate为今天,防止选过去的时间——这在预约场景里很常见。

注意:如果用户选了日期后取消时间选择,那日期选择的结果也要作废。所以上面代码里,如果timenull,整个返回null

4.5 实战避坑指南

我在项目中遇到过几个坑,今天一并告诉你:

  1. 日期范围限制firstDatelastDate一定要设置合理。我曾经没设lastDate,结果用户选到了3000年,数据库直接报错。
  2. 空值处理:用户可能取消选择,返回null。一定要做判空,否则null!会崩溃。
  3. 时区问题DateTime.now()是本地时间,但如果你要传给后端,最好转成UTC。用picked.toUtc()
  4. 键盘弹出:记得设置readOnly: true,否则用户点击输入框会弹出键盘,体验很差。
  5. 表单验证:日期字段的验证不能只看TextEditingController的文本,要检查_selectedDate是否为null

我曾经犯过一个低级错误:在validator里只判断了_dateController.text.isEmpty,结果用户选了日期后,文本不为空,但_selectedDate还是null。后来排查了半天才发现,是setState里忘了赋值。所以记住:文本和值要同步更新

4.6 进阶:自定义日期选择器样式

如果你觉得原生的日期选择器太丑,可以自定义主题。Flutter的showDatePicker支持builder参数,可以包裹一层Theme

final picked = await showDatePicker(
  context: context,
  initialDate: DateTime.now(),
  firstDate: DateTime(1900),
  lastDate: DateTime(2100),
  builder: (context, child) {
    return Theme(
      data: Theme.of(context).copyWith(
        colorScheme: ColorScheme.light(
          primary: Colors.blue, // 主色调
          onPrimary: Colors.white, // 文字颜色
        ),
      ),
      child: child!,
    );
  },
);

这样就能让日期选择器的颜色和你的App主题保持一致。时间选择器也同理。

4.7 总结

日期和时间选择器,说白了就是“选”代替“填”。核心就三点:

  • showDatePickershowTimePicker弹出选择器
  • TextEditingController展示格式化后的值
  • 用单独的变量存真正的日期时间值

嗯,掌握了这些,你在表单里集成日期时间选择器应该没问题了。下一章我们会讲更复杂的交互——多步骤表单,到时候这些基础组件都会用上。