Flutter 实战:world_clock 世界时钟的城市列表、UTC 偏移与鸿蒙适配解析
Flutter 实战:world_clock 世界时钟的城市列表、UTC 偏移与鸿蒙适配解析
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
world_clock 是一个基于 Flutter 实现的世界时钟应用。它内置了多个城市,并支持添加自定义城市和 UTC 偏移。页面会实时显示每个城市的本地时间、日期、星期以及按昼夜变化的图标和颜色,从而构成一个轻量但完整的跨时区展示工具。
本文基于项目真实源码展开,重点分析 城市列表结构、UTC 偏移计算、本地时间格式化、昼夜视觉提示、城市新增与删除、控制器生命周期 和 鸿蒙适配关注点。文章内容可直接发布到 CSDN,不包含面向作者的检查说明。
世界时钟看起来只是“把时间换算一下”,但真正的关键在于如何把城市、时区、偏移、日期和视觉状态组织成一条稳定的数据流。
world_clock很适合用来拆解这条链路。

图示说明:本文围绕 Flutter 世界时钟的城市列表、UTC 偏移、昼夜图标和跨端适配展开,适合用于鸿蒙、Android、iOS 等多端应用开发复盘。
一、项目定位与功能概览
1.1 应用主题
world_clock 的定位是一个 世界城市时间管理工具。用户可以查看多个城市的当前时间,也可以新增自定义城市并手动输入 UTC 偏移。
核心功能如下:
| 功能 | 页面表现 | 源码实现 |
|---|---|---|
| 预置城市 | New York、London、Tokyo 等 | _locations |
| 城市新增 | 顶部输入城市名和偏移 | _addLocation() |
| 城市删除 | 右侧删除按钮 | _removeLocation() |
| 当前时间 | 以 HH:mm:ss 显示 |
_getTime() |
| 当前日期 | 以 day/month/year 显示 |
_getDate() |
| 当前星期 | 显示 Monday 到 Sunday | _getDayName() |
| 昼夜图标 | 太阳、云、夜晚、深夜图标 | _getTimeIcon() |
| 昼夜颜色 | 不同时间段不同主色 | _getTimeColor() |
1.2 默认数据
项目启动后已有几个示例城市:
| 城市 | 时区 | 偏移 |
|---|---|---|
| New York | America/New_York | -5 |
| London | Europe/London | 0 |
| Tokyo | Asia/Tokyo | 9 |
| Sydney | Australia/Sydney | 10 |
| Dubai | Asia/Dubai | 4 |
这些默认数据让页面加载后就能展示多个城市的对比时间,便于验证列表、时间刷新和昼夜样式。
1.3 适合学习的点
这个项目适合学习以下 Flutter 实战能力:
- 如何用
List<Map<String, dynamic>>保存轻量时区列表。 - 如何根据 UTC 偏移计算本地时间。
- 如何在列表里同时展示时间、日期、星期和图标。
- 如何用颜色与图标表达昼夜状态。
- 如何新增、删除并维护城市列表。
- 如何面向鸿蒙验证输入、列表和布局适配。
二、工程结构与运行方式
2.1 工程结构
项目保持标准 Flutter 工程结构,核心代码集中在 lib/main.dart:
| 文件或目录 | 作用 | 说明 |
|---|---|---|
lib/main.dart |
应用入口与页面实现 | 包含城市列表、时间换算和 UI |
pubspec.yaml |
依赖声明 | 使用 Flutter SDK 与 Material 图标 |
test/widget_test.dart |
Widget 测试入口 | 可扩展为时间列表测试 |
ohos/ |
鸿蒙平台工程目录 | 用于跨端构建和适配 |
2.2 依赖声明
项目没有引入复杂第三方依赖:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
这说明时间计算、列表渲染和城市管理都由 Flutter 与 Dart 完成,不依赖系统时区库或平台通道。
2.3 常用命令
开发和验证时可以使用以下命令:
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 作用 | 使用场景 |
|---|---|---|
flutter pub get |
获取依赖 | 首次运行或依赖变化 |
flutter analyze |
静态分析 | 检查语法和 lint |
flutter test |
执行测试 | 验证 Widget 行为 |
flutter run |
启动应用 | 本地调试界面 |
三、应用入口与主题配置
3.1 main 函数
应用入口保持 Flutter 标准写法:
void main() {
runApp(const MyApp());
}
世界时钟不需要启动时初始化网络或数据库,因此入口很简洁。
3.2 MyApp 根组件
根组件负责创建 MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'World Clock',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
home: const MyHomePage(title: 'World Clock'),
);
}
}
这里有三个关键信息:
- 应用标题是
World Clock。 - 主题种子色是
Colors.indigo。 - 首页是
MyHomePage。
3.3 主题色选择
靛蓝色适合时间类工具,稳重且清晰。源码中列表头像背景、AppBar 和图标都围绕时间状态变化,整体观感统一。
当前源码没有显式设置
useMaterial3: true,因此文章以真实代码为准。如果后续需要统一 Material 3 表现,可以在ThemeData中补充该配置。
四、状态字段设计
4.1 核心状态总览
页面状态集中在 _MyHomePageState 中:
final List<Map<String, dynamic>> _locations = [
{'name': 'New York', 'timezone': 'America/New_York', 'offset': -5},
{'name': 'London', 'timezone': 'Europe/London', 'offset': 0},
];
final TextEditingController _cityController = TextEditingController();
final TextEditingController _offsetController = TextEditingController();
4.2 状态字段说明
| 字段 | 类型 | 作用 |
|---|---|---|
_locations |
List<Map<String, dynamic>> |
城市、时区和偏移列表 |
_cityController |
TextEditingController |
城市名称输入 |
_offsetController |
TextEditingController |
UTC 偏移输入 |
4.3 为什么使用列表而不是单一模型
当前项目使用 Map<String, dynamic> 保存城市信息,优点是写法简单、上手快:
| 优点 | 说明 |
|---|---|
| 简洁 | 少量字段直接存 Map |
| 灵活 | 便于快速增删字段 |
| 适合演示 | 适合教学和小工具样例 |
4.4 结构扩展建议
如果后续字段增多,可以把城市项抽成模型,例如:
class CityClock {
const CityClock({
required this.name,
required this.timezone,
required this.offset,
});
final String name;
final String timezone;
final int offset;
}
五、控制器生命周期管理
5.1 两个输入控制器
页面使用两个控制器管理输入:
final TextEditingController _cityController = TextEditingController();
final TextEditingController _offsetController = TextEditingController();
5.2 dispose 释放资源
页面销毁时释放控制器:
void dispose() {
_cityController.dispose();
_offsetController.dispose();
super.dispose();
}
5.3 生命周期表
| 阶段 | 行为 | 对应代码 |
|---|---|---|
| 页面创建 | 创建控制器 | 字段初始化 |
| 用户输入 | 控制器保存文本 | 顶部输入区 |
| 添加城市 | 读取并清空控制器 | _addLocation() |
| 页面销毁 | 释放控制器 | dispose() |
六、城市新增逻辑
6.1 addLocation 方法
新增城市的逻辑如下:
void _addLocation() {
if (_cityController.text.isEmpty) return;
final offset = int.tryParse(_offsetController.text) ?? 0;
setState(() {
_locations.add({
'name': _cityController.text,
'timezone': 'Custom',
'offset': offset,
});
_cityController.clear();
_offsetController.clear();
});
}
6.2 输入校验
代码只校验城市名不能为空:
if (_cityController.text.isEmpty) return;
6.3 偏移默认值
UTC 偏移使用:
final offset = int.tryParse(_offsetController.text) ?? 0;
如果用户没有输入有效数字,就默认按 0 处理。
6.4 新增后的状态变化
成功新增后会发生三件事:
_locations增加一项。- 城市输入框清空。
- 偏移输入框清空。
七、城市删除逻辑
7.1 removeLocation 方法
删除逻辑非常直接:
void _removeLocation(int index) {
setState(() {
_locations.removeAt(index);
});
}
7.2 删除入口
列表项右侧有删除按钮:
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeLocation(index),
)
7.3 删除后的界面变化
删除后列表会重新构建,少掉对应城市卡片。
7.4 删除交互边界
当前删除是即时生效,没有确认弹窗。对于演示型应用足够简单;如果做正式产品,可以增加撤销或确认提示。
八、时间计算核心
8.1 getTime 方法
本地时间由 _getTime() 计算:
String _getTime(int offset) {
final now = DateTime.now().toUtc();
final localTime = now.add(Duration(hours: offset));
return '${localTime.hour.toString().padLeft(2, '0')}:${localTime.minute.toString().padLeft(2, '0')}:${localTime.second.toString().padLeft(2, '0')}';
}
8.2 UTC 加小时偏移
核心公式是:
localTime = DateTime.now().toUtc().add(Duration(hours: offset));
这意味着:
| 偏移 | 结果 |
|---|---|
-5 |
比 UTC 早 5 小时 |
0 |
与 UTC 相同 |
9 |
比 UTC 晚 9 小时 |
8.3 补零显示
输出格式使用 padLeft(2, '0'):
hour.toString().padLeft(2, '0')
这保证小时、分钟和秒都以两位显示。
8.4 时间刷新策略
源码没有定时器,时间是每次构建时重新计算。因此只要界面刷新,显示时间就是最新的 UTC 偏移结果。
九、日期与星期计算
9.1 getDate 方法
日期由 _getDate() 计算:
String _getDate(int offset) {
final now = DateTime.now().toUtc();
final localTime = now.add(Duration(hours: offset));
return '${localTime.day}/${localTime.month}/${localTime.year}';
}
9.2 getDayName 方法
星期由 _getDayName() 计算:
String _getDayName(int offset) {
final now = DateTime.now().toUtc();
final localTime = now.add(Duration(hours: offset));
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
return days[localTime.weekday - 1];
}
9.3 星期名称映射
| weekday | 名称 |
|---|---|
| 1 | Monday |
| 2 | Tuesday |
| 3 | Wednesday |
| 4 | Thursday |
| 5 | Friday |
| 6 | Saturday |
| 7 | Sunday |
9.4 日期和星期的作用
日历类展示不只要显示时间,还要显示日期和星期,帮助用户快速判断某个城市现在处于哪一天、哪一个时段。
十、昼夜视觉提示
10.1 颜色判断
时间颜色由 _getTimeColor() 决定:
Color _getTimeColor(int offset) {
final now = DateTime.now().toUtc();
final hour = now.add(Duration(hours: offset)).hour;
if (hour >= 6 && hour < 12) return Colors.orange;
if (hour >= 12 && hour < 18) return Colors.blue;
if (hour >= 18 && hour < 22) return Colors.deepOrange;
return Colors.indigo;
}
10.2 图标判断
图标由 _getTimeIcon() 决定:
IconData _getTimeIcon(int offset) {
final now = DateTime.now().toUtc();
final hour = now.add(Duration(hours: offset)).hour;
if (hour >= 6 && hour < 12) return Icons.wb_sunny;
if (hour >= 12 && hour < 18) return Icons.wb_cloudy;
if (hour >= 18 && hour < 22) return Icons.nights_stay;
return Icons.dark_mode;
}
10.3 昼夜规则表
| 时间段 | 颜色 | 图标 |
|---|---|---|
| 6:00 - 11:59 | orange | wb_sunny |
| 12:00 - 17:59 | blue | wb_cloudy |
| 18:00 - 21:59 | deepOrange | nights_stay |
| 其他时间 | indigo | dark_mode |
10.4 视觉提示价值
用户不必精确读时分秒,也能通过颜色和图标快速感知城市是白天还是夜晚,这对世界时钟类应用很有帮助。
十一、城市卡片布局
11.1 列表结构
城市列表用 ListView.builder:
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _locations.length,
itemBuilder: (context, index) { ... },
),
)
11.2 城市头像
每个城市卡片左侧有一个圆形图标容器:
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _getTimeColor(location['offset']).withAlpha(50),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
_getTimeIcon(location['offset']),
size: 32,
color: _getTimeColor(location['offset']),
),
)
11.3 卡片中间信息
中间区域展示城市名、星期和 UTC 偏移:
Text(location['name'])
Text(_getDayName(location['offset']))
Text('UTC+9')
11.4 卡片右侧时间
右侧展示当前时间和日期:
Text(_getTime(location['offset']))
Text(_getDate(location['offset']))
11.5 信息层级
| 区域 | 内容 |
|---|---|
| 左侧 | 昼夜图标 |
| 中间 | 城市、星期、UTC 偏移 |
| 右侧 | 当前时间和日期 |
十二、顶部新增城市表单
12.1 Add City 卡片
顶部输入区是一张 Card:
Card(
margin: const EdgeInsets.all(16),
color: Colors.indigo.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(...),
),
)
12.2 城市输入框
城市名使用普通文本输入:
TextField(
controller: _cityController,
decoration: InputDecoration(
labelText: 'City',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
)
12.3 UTC 偏移输入框
偏移输入框使用数字键盘:
TextField(
controller: _offsetController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'UTC+',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
)
12.4 添加按钮
添加按钮使用图标按钮:
IconButton(
onPressed: _addLocation,
icon: const Icon(Icons.add_circle, color: Colors.indigo),
)
十三、日期时间展示的同步性
13.1 每个城市都独立计算
页面列表中每个城市都会单独调用时间、日期、星期和图标计算方法,因此不会共享缓存状态。
13.2 计算入口统一
所有时间展示都以 DateTime.now().toUtc() 为入口:
final now = DateTime.now().toUtc();
13.3 统一入口的好处
这样可以保证同一帧内各城市都基于同一个 UTC 参考点计算,逻辑清楚。
13.4 城市展示表
| 展示项 | 计算方式 |
|---|---|
| 时间 | UTC + offset |
| 日期 | localTime.day/month/year |
| 星期 | localTime.weekday 映射 |
| 图标 | hour 区间判断 |
十四、鸿蒙适配关注点
14.1 为什么适配风险较低
world_clock 主要由 Flutter 标准组件和 Dart 逻辑构成,不依赖数据库、网络、定位、系统时钟插件或平台通道,因此基础适配风险较低。
| 模块 | 是否依赖平台能力 | 适配关注度 |
|---|---|---|
| 时间计算 | Dart 标准库 | 低 |
| 列表展示 | Flutter 标准组件 | 低 |
| 文本输入 | Flutter 标准输入 | 中 |
| 滚动布局 | Flutter 标准组件 | 低 |
| 本地持久化 | 当前未实现 | 高 |
14.2 输入法与软键盘
鸿蒙设备上需要重点验证:
- 城市名输入是否顺畅。
- 数字键盘是否正常弹出。
- 输入框在软键盘弹出后是否仍可见。
- 添加后输入框清空是否正常。
14.3 列表适配
每个城市卡片包含三列信息:图标、城市信息、时间信息。小屏设备上要重点观察文字是否换行、是否出现溢出。
14.4 数据边界
当前城市列表只保存在内存中,应用重启后新增城市不会保留。若要做正式世界时钟工具,应增加本地存储或同步能力。
当前项目适合作为 Flutter 世界时钟样例,不应理解为具备长期数据保存和系统时区管理能力的完整产品。
十五、测试设计与默认测试改造
15.1 当前测试入口
项目中的测试文件仍是默认计数器测试。对于世界时钟应用,更有价值的是验证初始城市列表、新增城市、删除城市和时间展示。
15.2 初始页面测试
testWidgets('world clock renders initial cities', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('World Clock'), findsWidgets);
expect(find.text('New York'), findsOneWidget);
expect(find.text('Tokyo'), findsOneWidget);
});
15.3 添加城市测试
testWidgets('can add a city', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.enterText(find.byType(TextField).first, 'Paris');
await tester.enterText(find.byType(TextField).last, '1');
await tester.tap(find.byIcon(Icons.add_circle));
await tester.pump();
expect(find.text('Paris'), findsOneWidget);
});
15.4 删除城市测试
testWidgets('can delete a city', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.byIcon(Icons.delete).first);
await tester.pump();
expect(find.text('New York'), findsNothing);
});
15.5 时间格式测试
如果把 _getTime() 抽成纯函数,可以直接测试 HH:mm:ss 格式是否正确。
十六、可维护性优化方向
16.1 抽离城市模型
当前使用 Map 保存城市,简单但类型不够明确。可以抽成模型:
class WorldCity {
const WorldCity({
required this.name,
required this.timezone,
required this.offset,
});
final String name;
final String timezone;
final int offset;
}
16.2 增加本地持久化
如果希望新增城市在重启后保留,可以考虑:
| 方案 | 适合场景 |
|---|---|
| SharedPreferences | 少量偏好设置 |
| SQLite | 结构化城市列表 |
| 文件存储 | 简单导入导出 |
| 云同步 | 多设备同步 |
16.3 统一时间计算方法
当前 _getTime()、_getDate()、_getDayName() 都重复计算 localTime。可以抽出公共方法,减少重复。
16.4 增加搜索与排序
若城市数量增多,可以支持按城市名搜索、按时差排序或按当前时间排序。
十七、功能扩展方向
17.1 增加真实时区支持
现在项目只使用 UTC 偏移,不处理夏令时和复杂时区规则。后续可以引入更完整的时区库。
17.2 增加世界地图或城市分组
可以按洲、国家或区域分组展示城市,提高城市多时的可读性。
17.3 增加收藏城市
可以把常看的城市置顶或收藏,减少滚动查找。
17.4 增加自动刷新
当前界面是构建时计算时间,如果要让秒数持续跳动,可以增加定时刷新机制。
十八、常见问题与优化建议
18.1 为什么城市名不能为空
源码在 _addLocation() 中判断:
if (_cityController.text.isEmpty) return;
城市名为空时不会新增记录。
18.2 为什么偏移为空时默认为 0
使用:
int.tryParse(_offsetController.text) ?? 0;
这样可以避免非法输入导致异常。
18.3 为什么列表里没有定时自动刷新
当前项目是手动构建刷新时间显示,没有引入定时器。它适合作为时间换算演示,但不是严格的实时时钟产品。
18.4 为什么图标和颜色根据小时变化
这样能帮助用户快速判断城市处于白天还是夜晚,提高可读性。
18.5 鸿蒙适配最应该关注什么
重点关注数字输入、列表滚动、卡片布局、软键盘遮挡和新增删除交互。当前项目没有持久化,新增城市不会在重启后保留。
十九、完整流程复盘
19.1 页面启动流程
main()
-> runApp(MyApp)
-> MaterialApp
-> MyHomePage
-> initState 设置默认城市
-> build 渲染列表
19.2 新增城市流程
输入城市名
-> 输入 UTC 偏移
-> 点击 add
-> 校验城市名
-> 解析偏移
-> 添加到 _locations
-> 清空输入框
19.3 删除城市流程
点击删除按钮
-> _removeLocation(index)
-> removeAt 删除记录
-> setState 刷新列表
19.4 时间展示流程
读取 UTC 时间
-> 加上偏移
-> 格式化为时间、日期、星期
-> 根据小时计算颜色和图标
-> 刷新城市卡片
二十、相关资源与继续学习
20.1 Flutter 学习资源
世界时钟涉及列表、输入、时间计算和测试,可以结合以下资源学习:
| 资源 | 内容 |
|---|---|
| Flutter Docs | Flutter 官方开发文档 |
| Dart 官方文档 | Dart 语言与核心库 |
| Widget catalog | Flutter 常用组件 |
| Flutter testing | Widget 测试与交互模拟 |
20.2 世界时钟扩展方向
后续可以继续增强:
- 更真实的时区库。
- 自动刷新秒数。
- 城市收藏。
- 城市分组。
- 搜索和排序。
- 本地持久化。
- 深色模式优化。
20.3 跨端实践价值
world_clock 很适合作为 Flutter 适配鸿蒙的小型时间工具样例。它依赖很轻,但覆盖了输入、列表、时间换算、昼夜视觉和滚动布局,能帮助开发者验证很多跨端基础能力。
总结
world_clock 用简洁的 Flutter 代码实现了一个世界时钟列表工具。它通过 _locations 保存城市信息,通过 UTC 偏移换算本地时间,通过 _getDate() 和 _getDayName() 展示日期与星期,通过 _getTimeColor() 和 _getTimeIcon() 提供昼夜视觉提示,并支持新增和删除城市。
从工程角度看,这个项目最值得学习的是“城市列表 + UTC 偏移 + 昼夜视觉反馈”的组合方式。面向鸿蒙适配时,项目依赖较轻,主要需要验证数字输入、列表滚动、时间展示和触摸反馈。对于想学习 Flutter 时间类应用的开发者来说,它是一个清晰、实用且容易扩展的案例。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐



所有评论(0)