4、日期与时间选择器:DatePicker与TimePicker在表单中的集成
日期和时间选择,几乎是每个表单都绕不开的坎。我记得刚做Flutter那会儿,有个用户注册的页面,用户要填出生日期和预约时间。我直接用了个TextField让用户手打,结果上线第一天就被骂惨了——格式五花八门,有人写“2024-3-5”,有人写“24年3月5号”,还有人写“三月五号”。嗯,从那以后,我再也不敢让用户手动输入日期了。
说白了,日期时间选择器就是帮用户“选”而不是“填”的组件。Flutter原生提供了showDatePicker和showTimePicker,用起来很方便。但真正集成到表单里,还是有不少细节要注意的。今天我就把实战中积累的经验分享给你。
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为今天,防止选过去的时间——这在预约场景里很常见。
注意:如果用户选了日期后取消时间选择,那日期选择的结果也要作废。所以上面代码里,如果time为null,整个返回null。
4.5 实战避坑指南
我在项目中遇到过几个坑,今天一并告诉你:
- 日期范围限制:
firstDate和lastDate一定要设置合理。我曾经没设lastDate,结果用户选到了3000年,数据库直接报错。 - 空值处理:用户可能取消选择,返回
null。一定要做判空,否则null!会崩溃。 - 时区问题:
DateTime.now()是本地时间,但如果你要传给后端,最好转成UTC。用picked.toUtc()。 - 键盘弹出:记得设置
readOnly: true,否则用户点击输入框会弹出键盘,体验很差。 - 表单验证:日期字段的验证不能只看
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 总结
日期和时间选择器,说白了就是“选”代替“填”。核心就三点:
- 用
showDatePicker和showTimePicker弹出选择器 - 用
TextEditingController展示格式化后的值 - 用单独的变量存真正的日期时间值
嗯,掌握了这些,你在表单里集成日期时间选择器应该没问题了。下一章我们会讲更复杂的交互——多步骤表单,到时候这些基础组件都会用上。