通用 Flutter 日期时间选择器 (AppDatePicker)
一个功能强大、高度可定制、易于使用的 Flutter 日期和时间选择器组件。通过简单统一的 API,可以实现多种选择模式,满足绝大部分应用场景的需求。
✨ 特性
- 多种选择模式:
- 📅 编辑日期 (
editDate): 完整选择年、月、日、时、分。 - ⏳ 编辑时间 (
editTime): 仅选择时、分。 - 🚀 开始/结束时间 (
startTime/endTime): 仅选择年、月、日,常用于设置日期范围。 - 🔍 筛选模式 (
filterDate): 按 周、月 或 年 进行快速筛选。
- 📅 编辑日期 (
- 高度可定制:
- 支持设置可选的最小开始时间 (
startTime)。 - 可限制选择范围,不允许选择未来时间 (
showLaterTime: false)。 - 支持设置筛选模式下的回溯年限 (
yearsBack)。
- 支持设置可选的最小开始时间 (
- 优秀的开发者体验:
- 通过静态方法
AppDatePicker.show()一行代码即可调用。 - 统一的回调接口
onConfirm获取最终选择结果。 - 实时变化回调
onChange,方便监听选择过程。
- 通过静态方法
- 精心设计的 UI:
- 简洁、现代化的设计,适配亮色模式。
- 滚动选择器带有清晰的选中状态和动画效果。
📐 设计理念与实现原理
这个日期选择器在重构过程中遵循了几个核心设计原则:
-
分层与解耦 (Layering & Decoupling)
- 底层 (
CustomPicker): 这是最核心的滚动选择器引擎。它被设计成一个“哑组件”(Dumb Component),完全不关心业务逻辑。它只负责提供一个可滚动的列表,并通过itemBuilder回调将列表项的 UI 构建权完全交还给调用方。这使得CustomPicker具备极高的复用性,可以用于任何需要滚动选择的场景。 - 业务逻辑层 (
EditDate,FilterDate,EditTime): 这些组件基于CustomPicker构建,封装了各自的业务逻辑。例如,EditDate负责处理年、月、日、时、分之间的联动和约束关系(如二月没有30号,选择的时间不能早于开始时间等)。 - 入口/展示层 (
AppDatePicker,DatePickerOverlay): 这是暴露给开发者的最顶层 API。AppDatePicker.show()使用Overlay来创建一个全局浮层,将整个组件呈现在屏幕上,并管理其生命周期。它将复杂的实现细节完全隐藏,提供了一个极其简洁的调用方式。
- 底层 (
-
统一状态管理 (Centralized State Management)
- 在
EditDate组件中,我们摒弃了分散管理selectedYear,selectedMonth,selectedDay等多个状态变量的做法。取而代之的是,使用一个单一的DateTime对象_selectedDate来作为唯一的数据源 (Single Source of Truth)。 - 当任何一个选择器(如年份选择器)的值发生变化时,我们会创建一个新的
DateTime实例,然后通过一个统一的_constrainDate方法来修正这个值,确保其合法性,最后通过setState更新_selectedDate。这种方式让状态管理更集中、逻辑更清晰、代码更健壮。
- 在
-
组合优于继承 (Composition over Inheritance)
- 整个组件是 छोटे widgets 组合而成的。
DatePickerOverlay根据传入的mode动态地组合EditDate、FilterDate或EditTime组件,而不是通过继承来扩展功能。这种方式使得代码结构更加灵活和易于维护。
- 整个组件是 छोटे widgets 组合而成的。
🔧 使用方法
1. 导入
import 'path/to/your/date_picker.dart';
2. 调用 AppDatePicker.show()
示例 1: 编辑日期 (可选择未来时间)
ElevatedButton(
onPressed: () => AppDatePicker.show(
context: context,
mode: AppDatePickerMode.editDate,
initialDateTime: DateTime(2024, 2, 18, 17, 12),
showLaterTime: true, // 允许选择未来的时间
onConfirm: (dateTime) {
// dateTime 是一个 DateTime 对象
print("确定时间: $dateTime");
},
onChange: (dateTime) {
print("选择变化: $dateTime");
},
),
child: const Text("编辑日期"),
),
示例 2: 筛选时间 (按周)
ElevatedButton(
onPressed: () => AppDatePicker.show(
context: context,
mode: AppDatePickerMode.filterDate,
filterType: FilterType.week,
initialDateTime: DateTime(2024, 12, 2, 17, 22),
onConfirm: (dateRange) {
// dateRange 是一个 Map<String, DateTime>
// e.g., {startTime: 2024-12-02 00:00:00.000, endTime: 2024-12-08 00:00:00.000}
print("确定范围: $dateRange");
},
),
child: const Text("筛选时间-周"),
),
示例 3: 设置结束时间 (带开始时间限制)
// 假设 startTime 是一个已选的 DateTime
DateTime startTime = DateTime(2024, 6, 20);
DateTime endTime = DateTime(2024, 7, 5);
ElevatedButton(
onPressed: () {
AppDatePicker.show(
context: context,
mode: AppDatePickerMode.endTime,
initialDateTime: endTime,
startTime: startTime, // 传入开始时间,结束时间不能早于它
showLaterTime: false, // 不允许选择未来
onConfirm: (date) {
// date 是一个 DateTime 对象,时分秒为0
setState(() {
endTime = date;
});
},
);
},
child: Text("选择结束时间"),
),
示例 4: 自定义筛选周UI和标题
ElevatedButton(
onPressed: () {
AppDatePicker.show(
context: context,
mode: AppDatePickerMode.filterDate,
filterType: FilterType.week,
title: "选择统计周期",
onConfirm: (result) {},
weekItemBuilder: (context, weekData, isSelected) {
final dateRange = weekData.split('(').first;
final isThisWeek = weekData.contains('本周');
return Card(
color: isSelected ? Colors.blue.shade50 : Colors.white,
elevation: isSelected ? 2 : 0,
child: Center(
child: Row(
mainAxisAlignment:MainAxisAlignment.center,
children: [
Text(
dateRange,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue.shade800 : Colors.grey.shade700,
),
),
if(isThisWeek)
const Icon(Icons.star, color: Colors.amber, size: 16)
],
),
),
);
},
);
},
child: const Text('筛选时间-周 (自定义UI)'),
),
📚 API 参考
AppDatePicker.show() 方法的主要参数:
| 参数 | 类型 | 描述 | 是否必须 |
|---|---|---|---|
context |
BuildContext |
构建上下文 | 是 |
mode |
AppDatePickerMode |
选择器模式 | 是 |
onConfirm |
Function(dynamic) |
点击"确定"的回调 | 是 |
initialDateTime |
DateTime? |
初始显示的日期时间,默认为 DateTime.now() |
否 |
filterType |
FilterType? |
当 mode 为 filterDate 时,指定筛选类型 |
否 |
startTime |
DateTime? |
可选的最小开始时间,用于限制选择范围 | 否 |
showLaterTime |
bool? |
是否允许选择未来的时间,默认为 true |
否 |
yearsBack |
int? |
filterDate 模式下回溯的年数,默认 3 |
否 |
minYear |
int? |
可选的最小年份,用于限制年份选择范围。未提供时使用默认逻辑 | 否 |
maxYear |
int? |
可选的最大年份,用于限制年份选择范围。未提供时使用默认逻辑(showLaterTime 为 true 时默认为当前年份+10年,否则为当前年份) |
否 |
onChange |
Function(dynamic)? |
选择值变化时的实时回调 | 否 |
onCancel |
Function()? |
点击"取消"或遮罩时的回调 | 否 |
weekItemBuilder |
WeekItemBuilder? |
week子组件渲染函数 | 否 |
title |
String? |
自定义标题 | 否 |
🚀 未来规划
支持国际化 (i18n),如显示 "Year", "Month" 等。支持自定义主题颜色。增加更多的日期格式化选项。
📜 开源协议
该项目基于 MIT 协议开源。