【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战

Flutter for OpenHarmony 闹钟时钟应用开发实战

社区引导信息

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

作者:maaath

前言

在移动应用开发领域,跨平台框架一直是开发者们关注的焦点。Flutter作为Google推出的UI框架,凭借其高性能和一致的用户体验,已经成为跨平台开发的首选方案之一。而当Flutter遇上OpenHarmony,会碰撞出怎样的火花?本文将通过一个完整的闹钟时钟应用实例,带大家深入了解Flutter for OpenHarmony的实际开发流程。


一、项目概述

本文将手把手教你使用Flutter for OpenHarmony开发一个功能完善的闹钟时钟应用。该应用具备以下核心功能:

  • 闹钟创建、编辑、删除与开关控制
  • 重复周期设置(单次、每天、工作日、周末、自定义)
  • 闹钟铃声选择与震动模式设置
  • 世界时钟切换
  • 秒表计时功能
  • 倒计时功能
  • 睡眠记录统计

1.1 技术栈

  • 框架:Flutter 4.0+ (适配OpenHarmony)
  • 状态管理:Riverpod / Provider
  • 本地存储:SharedPreferences
  • 通知服务:Flutter Local Notifications
  • 平台通道:MethodChannel(用于原生能力调用)

二、项目结构设计

良好的项目结构是大型应用开发的基础。我们采用功能模块化的设计理念:

复制代码
lib/
├── main.dart                 # 应用入口
├── models/                   # 数据模型
│   ├── alarm_model.dart
│   ├── world_clock_model.dart
│   └── sleep_record_model.dart
├── providers/               # 状态管理
│   ├── alarm_provider.dart
│   ├── stopwatch_provider.dart
│   └── countdown_provider.dart
├── screens/                 # 页面
│   ├── home_screen.dart
│   ├── alarm_screen.dart
│   ├── world_clock_screen.dart
│   ├── stopwatch_screen.dart
│   ├── countdown_screen.dart
│   └── sleep_screen.dart
├── widgets/                 # 通用组件
│   ├── time_picker.dart
│   └── circular_timer.dart
└── services/                # 服务层
    ├── notification_service.dart
    └── storage_service.dart

三、核心数据模型

3.1 闹钟模型

dart 复制代码
// models/alarm_model.dart

enum RepeatType {
  once,       // 单次
  daily,      // 每天
  weekdays,   // 工作日
  weekend,    // 周末
  custom      // 自定义
}

class AlarmModel {
  final String id;
  final int hour;
  final int minute;
  final String label;
  final bool isEnabled;
  final RepeatType repeatType;
  final List<int> customRepeatDays; // 0=周日,1=周一...6=周六
  final String ringtoneId;
  final String ringtoneName;
  final bool vibrate;

  AlarmModel({
    required this.id,
    required this.hour,
    required this.minute,
    this.label = '',
    this.isEnabled = true,
    this.repeatType = RepeatType.once,
    this.customRepeatDays = const [],
    this.ringtoneId = 'default',
    this.ringtoneName = '默认铃声',
    this.vibrate = true,
  });

  // 复制并修改
  AlarmModel copyWith({
    String? id,
    int? hour,
    int? minute,
    String? label,
    bool? isEnabled,
    RepeatType? repeatType,
    List<int>? customRepeatDays,
    String? ringtoneId,
    String? ringtoneName,
    bool? vibrate,
  }) {
    return AlarmModel(
      id: id ?? this.id,
      hour: hour ?? this.hour,
      minute: minute ?? this.minute,
      label: label ?? this.label,
      isEnabled: isEnabled ?? this.isEnabled,
      repeatType: repeatType ?? this.repeatType,
      customRepeatDays: customRepeatDays ?? this.customRepeatDays,
      ringtoneId: ringtoneId ?? this.ringtoneId,
      ringtoneName: ringtoneName ?? this.ringtoneName,
      vibrate: vibrate ?? this.vibrate,
    );
  }

  // 格式化时间显示
  String get formattedTime {
    final h = hour.toString().padLeft(2, '0');
    final m = minute.toString().padLeft(2, '0');
    return '$h:$m';
  }

  // 获取重复描述
  String get repeatDescription {
    switch (repeatType) {
      case RepeatType.once:
        return '不重复';
      case RepeatType.daily:
        return '每天';
      case RepeatType.weekdays:
        return '工作日';
      case RepeatType.weekend:
        return '周末';
      case RepeatType.custom:
        return _formatCustomDays();
    }
  }

  String _formatCustomDays() {
    if (customRepeatDays.isEmpty) return '不重复';
    final dayNames = ['日', '一', '二', '三', '四', '五', '六'];
    return customRepeatDays.map((d) => dayNames[d]).join('、');
  }

  // 序列化
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'hour': hour,
      'minute': minute,
      'label': label,
      'isEnabled': isEnabled,
      'repeatType': repeatType.index,
      'customRepeatDays': customRepeatDays,
      'ringtoneId': ringtoneId,
      'ringtoneName': ringtoneName,
      'vibrate': vibrate,
    };
  }

  // 反序列化
  factory AlarmModel.fromJson(Map<String, dynamic> json) {
    return AlarmModel(
      id: json['id'] as String,
      hour: json['hour'] as int,
      minute: json['minute'] as int,
      label: json['label'] as String? ?? '',
      isEnabled: json['isEnabled'] as bool? ?? true,
      repeatType: RepeatType.values[json['repeatType'] as int? ?? 0],
      customRepeatDays: (json['customRepeatDays'] as List<dynamic>?)
              ?.map((e) => e as int)
              .toList() ??
          [],
      ringtoneId: json['ringtoneId'] as String? ?? 'default',
      ringtoneName: json['ringtoneName'] as String? ?? '默认铃声',
      vibrate: json['vibrate'] as bool? ?? true,
    );
  }
}

3.2 世界时钟模型

dart 复制代码
// models/world_clock_model.dart

class WorldClockModel {
  final String id;
  final String cityName;
  final String cityNameZh;
  final String timezone;
  final int utcOffset; // 相对于UTC的偏移小时数
  final bool isLocalZone;

  WorldClockModel({
    required this.id,
    required this.cityName,
    required this.cityNameZh,
    required this.timezone,
    required this.utcOffset,
    this.isLocalZone = false,
  });

  // 获取该城市当前时间
  DateTime get currentTime {
    final now = DateTime.now();
    final utc = now.toUtc();
    return utc.add(Duration(hours: utcOffset));
  }

  // 格式化时间
  String get formattedTime {
    final time = currentTime;
    final h = time.hour.toString().padLeft(2, '0');
    final m = time.minute.toString().padLeft(2, '0');
    return '$h:$m';
  }

  // 格式化时区偏移
  String get offsetText {
    if (utcOffset == 0) return 'GMT';
    final sign = utcOffset > 0 ? '+' : '';
    return 'GMT$sign$utcOffset';
  }
}

// 预设的世界城市列表
class WorldClockPresets {
  static final List<WorldClockModel> presets = [
    WorldClockModel(
      id: 'beijing',
      cityName: 'Beijing',
      cityNameZh: '北京',
      timezone: 'Asia/Shanghai',
      utcOffset: 8,
      isLocalZone: true,
    ),
    WorldClockModel(
      id: 'tokyo',
      cityName: 'Tokyo',
      cityNameZh: '东京',
      timezone: 'Asia/Tokyo',
      utcOffset: 9,
    ),
    WorldClockModel(
      id: 'newyork',
      cityName: 'New York',
      cityNameZh: '纽约',
      timezone: 'America/New_York',
      utcOffset: -5,
    ),
    WorldClockModel(
      id: 'london',
      cityName: 'London',
      cityNameZh: '伦敦',
      timezone: 'Europe/London',
      utcOffset: 0,
    ),
    WorldClockModel(
      id: 'paris',
      cityName: 'Paris',
      cityNameZh: '巴黎',
      timezone: 'Europe/Paris',
      utcOffset: 1,
    ),
    WorldClockModel(
      id: 'sydney',
      cityName: 'Sydney',
      cityNameZh: '悉尼',
      timezone: 'Australia/Sydney',
      utcOffset: 10,
    ),
    WorldClockModel(
      id: 'dubai',
      cityName: 'Dubai',
      cityNameZh: '迪拜',
      timezone: 'Asia/Dubai',
      utcOffset: 4,
    ),
    WorldClockModel(
      id: 'singapore',
      cityName: 'Singapore',
      cityNameZh: '新加坡',
      timezone: 'Asia/Singapore',
      utcOffset: 8,
    ),
    WorldClockModel(
      id: 'seoul',
      cityName: 'Seoul',
      cityNameZh: '首尔',
      timezone: 'Asia/Seoul',
      utcOffset: 9,
    ),
    WorldClockModel(
      id: 'losangeles',
      cityName: 'Los Angeles',
      cityNameZh: '洛杉矶',
      timezone: 'America/Los_Angeles',
      utcOffset: -8,
    ),
  ];
}

四、状态管理实现

4.1 闹钟状态管理

dart 复制代码
// providers/alarm_provider.dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/alarm_model.dart';

class AlarmProvider extends ChangeNotifier {
  static const String _storageKey = 'alarms_data';
  List<AlarmModel> _alarms = [];
  SharedPreferences? _prefs;

  List<AlarmModel> get alarms => List.unmodifiable(_alarms);

  // 初始化
  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
    await _loadAlarms();
  }

  // 加载闹钟数据
  Future<void> _loadAlarms() async {
    final data = _prefs?.getString(_storageKey);
    if (data != null) {
      final List<dynamic> jsonList = json.decode(data);
      _alarms = jsonList.map((e) => AlarmModel.fromJson(e)).toList();
      _sortAlarms();
      notifyListeners();
    }
  }

  // 保存闹钟数据
  Future<void> _saveAlarms() async {
    final jsonList = _alarms.map((e) => e.toJson()).toList();
    await _prefs?.setString(_storageKey, json.encode(jsonList));
  }

  // 添加闹钟
  Future<void> addAlarm(AlarmModel alarm) async {
    _alarms.add(alarm);
    _sortAlarms();
    await _saveAlarms();
    notifyListeners();
  }

  // 更新闹钟
  Future<void> updateAlarm(AlarmModel alarm) async {
    final index = _alarms.indexWhere((a) => a.id == alarm.id);
    if (index != -1) {
      _alarms[index] = alarm;
      _sortAlarms();
      await _saveAlarms();
      notifyListeners();
    }
  }

  // 删除闹钟
  Future<void> deleteAlarm(String id) async {
    _alarms.removeWhere((a) => a.id == id);
    await _saveAlarms();
    notifyListeners();
  }

  // 切换闹钟开关
  Future<void> toggleAlarm(String id) async {
    final index = _alarms.indexWhere((a) => a.id == id);
    if (index != -1) {
      _alarms[index] = _alarms[index].copyWith(
        isEnabled: !_alarms[index].isEnabled,
      );
      await _saveAlarms();
      notifyListeners();
    }
  }

  // 获取下一个闹钟
  AlarmModel? getNextAlarm() {
    final enabled = _alarms.where((a) => a.isEnabled).toList();
    if (enabled.isEmpty) return null;

    final now = DateTime.now();
    final currentMinutes = now.hour * 60 + now.minute;

    enabled.sort((a, b) {
      final aTime = a.hour * 60 + a.minute;
      final bTime = b.hour * 60 + b.minute;
      return aTime.compareTo(bTime);
    });

    for (final alarm in enabled) {
      final alarmMinutes = alarm.hour * 60 + alarm.minute;
      if (alarmMinutes > currentMinutes) {
        return alarm;
      }
    }

    return enabled.first;
  }

  // 按时间排序
  void _sortAlarms() {
    _alarms.sort((a, b) {
      final aTime = a.hour * 60 + a.minute;
      final bTime = b.hour * 60 + b.minute;
      return aTime.compareTo(bTime);
    });
  }
}

4.2 秒表状态管理

dart 复制代码
// providers/stopwatch_provider.dart

import 'package:flutter/foundation.dart';

class LapRecord {
  final int lapNumber;
  final int elapsedTime; // 毫秒
  final int lapTime;      // 单圈时间

  LapRecord({
    required this.lapNumber,
    required this.elapsedTime,
    required this.lapTime,
  });
}

class StopwatchProvider extends ChangeNotifier {
  bool _isRunning = false;
  int _elapsedTime = 0;
  final List<LapRecord> _laps = [];
  DateTime? _startTime;

  bool get isRunning => _isRunning;
  int get elapsedTime => _elapsedTime;
  List<LapRecord> get laps => List.unmodifiable(_laps);

  // 格式化时间
  String get formattedTime {
    final totalSeconds = _elapsedTime ~/ 1000;
    final minutes = totalSeconds ~/ 60;
    final seconds = totalSeconds % 60;
    final centiseconds = (_elapsedTime % 1000) ~/ 10;

    return '${minutes.toString().padLeft(2, '0')}:'
           '${seconds.toString().padLeft(2, '0')}.'
           '${centiseconds.toString().padLeft(2, '0')}';
  }

  // 开始计时
  void start() {
    if (_isRunning) return;
    _isRunning = true;
    _startTime = DateTime.now();
    notifyListeners();
  }

  // 暂停
  void pause() {
    if (!_isRunning) return;
    _isRunning = false;
    notifyListeners();
  }

  // 重置
  void reset() {
    _isRunning = false;
    _elapsedTime = 0;
    _laps.clear();
    _startTime = null;
    notifyListeners();
  }

  // 计次
  void lap() {
    if (!_isRunning || _startTime == null) return;

    final now = DateTime.now();
    _elapsedTime = now.difference(_startTime!).inMilliseconds;

    final lastLapTime = _laps.isEmpty ? 0 : _laps.last.elapsedTime;
    _laps.add(LapRecord(
      lapNumber: _laps.length + 1,
      elapsedTime: _elapsedTime,
      lapTime: _elapsedTime - lastLapTime,
    ));

    notifyListeners();
  }

  // 更新计时(由Timer调用)
  void tick() {
    if (_isRunning && _startTime != null) {
      _elapsedTime = DateTime.now().difference(_startTime!).inMilliseconds;
      notifyListeners();
    }
  }

  // 格式化单圈时间
  String formatLapTime(int milliseconds) {
    final totalSeconds = milliseconds ~/ 1000;
    final minutes = totalSeconds ~/ 60;
    final seconds = totalSeconds % 60;
    final centiseconds = (milliseconds % 1000) ~/ 10;

    return '${minutes.toString().padLeft(2, '0')}:'
           '${seconds.toString().padLeft(2, '0')}.'
           '${centiseconds.toString().padLeft(2, '0')}';
  }
}

五、UI界面实现

5.1 主页面框架

dart 复制代码
// screens/home_screen.dart

import 'package:flutter/material.dart';
import 'alarm_screen.dart';
import 'world_clock_screen.dart';
import 'stopwatch_screen.dart';
import 'sleep_screen.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = const [
    AlarmScreen(),
    WorldClockScreen(),
    StopwatchScreen(),
    SleepScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _screens,
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.alarm_outlined),
            selectedIcon: Icon(Icons.alarm),
            label: '闹钟',
          ),
          NavigationDestination(
            icon: Icon(Icons.public_outlined),
            selectedIcon: Icon(Icons.public),
            label: '世界时钟',
          ),
          NavigationDestination(
            icon: Icon(Icons.timer_outlined),
            selectedIcon: Icon(Icons.timer),
            label: '秒表',
          ),
          NavigationDestination(
            icon: Icon(Icons.nightlight_outlined),
            selectedIcon: Icon(Icons.nightlight),
            label: '睡眠',
          ),
        ],
      ),
    );
  }
}

5.2 闹钟页面

dart 复制代码
// screens/alarm_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/alarm_provider.dart';
import '../models/alarm_model.dart';
import 'alarm_edit_screen.dart';

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

  @override
  State<AlarmScreen> createState() => _AlarmScreenState();
}

class _AlarmScreenState extends State<AlarmScreen> {
  String _currentTime = '';
  String _currentDate = '';

  @override
  void initState() {
    super.initState();
    _updateTime();
    // 每秒更新时间
    Future.doWhile(() async {
      await Future.delayed(const Duration(seconds: 1));
      if (mounted) {
        _updateTime();
        return true;
      }
      return false;
    });
  }

  void _updateTime() {
    final now = DateTime.now();
    setState(() {
      _currentTime = '${now.hour.toString().padLeft(2, '0')}:'
                     '${now.minute.toString().padLeft(2, '0')}';
      final weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
      _currentDate = '${weekdays[now.weekday % 7]} '
                     '${now.month}月${now.day}日';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: SafeArea(
        child: Column(
          children: [
            _buildHeader(),
            _buildTimeDisplay(),
            Expanded(child: _buildAlarmList()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _navigateToEdit(null),
        backgroundColor: const Color(0xFF00B4D8),
        child: const Icon(Icons.add, color: Colors.white),
      ),
    );
  }

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          const Text(
            '闹钟',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: Color(0xFF333333),
            ),
          ),
          const Spacer(),
          Text(
            _currentTime,
            style: const TextStyle(
              fontSize: 16,
              color: Color(0xFF666666),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTimeDisplay() {
    return Container(
      margin: const EdgeInsets.all(16),
      padding: const EdgeInsets.all(30),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.08),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        children: [
          Text(
            _currentTime,
            style: const TextStyle(
              fontSize: 72,
              fontWeight: FontWeight.w300,
              color: Color(0xFF333333),
            ),
          ),
          const SizedBox(height: 10),
          Text(
            _currentDate,
            style: const TextStyle(
              fontSize: 16,
              color: Color(0xFF999999),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildAlarmList() {
    return Consumer<AlarmProvider>(
      builder: (context, provider, child) {
        final alarms = provider.alarms;

        if (alarms.isEmpty) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text(
                  '🔔',
                  style: TextStyle(fontSize: 64),
                ),
                const SizedBox(height: 16),
                const Text(
                  '暂无闹钟',
                  style: TextStyle(
                    fontSize: 18,
                    color: Color(0xFF666666),
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  '点击下方按钮添加闹钟',
                  style: TextStyle(
                    fontSize: 14,
                    color: Color(0xFF999999),
                  ),
                ),
              ],
            ),
          );
        }

        return ListView.builder(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          itemCount: alarms.length,
          itemBuilder: (context, index) {
            final alarm = alarms[index];
            return _buildAlarmCard(alarm, provider);
          },
        );
      },
    );
  }

  Widget _buildAlarmCard(AlarmModel alarm, AlarmProvider provider) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 4,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: GestureDetector(
              onTap: () => _navigateToEdit(alarm),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    alarm.formattedTime,
                    style: TextStyle(
                      fontSize: 36,
                      fontWeight: FontWeight.w300,
                      color: alarm.isEnabled
                          ? const Color(0xFF333333)
                          : const Color(0xFF999999),
                    ),
                  ),
                  if (alarm.label.isNotEmpty) ...[
                    const SizedBox(height: 4),
                    Text(
                      alarm.label,
                      style: const TextStyle(
                        fontSize: 14,
                        color: Color(0xFF666666),
                      ),
                    ),
                  ],
                  const SizedBox(height: 4),
                  Text(
                    alarm.repeatDescription,
                    style: const TextStyle(
                      fontSize: 12,
                      color: Color(0xFF999999),
                    ),
                  ),
                ],
              ),
            ),
          ),
          Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Row(
                children: [
                  Text(
                    alarm.ringtoneName,
                    style: const TextStyle(
                      fontSize: 12,
                      color: Color(0xFF999999),
                    ),
                  ),
                  if (alarm.vibrate) ...[
                    const SizedBox(width: 8),
                    const Icon(
                      Icons.vibration,
                      size: 16,
                      color: Color(0xFF999999),
                    ),
                  ],
                ],
              ),
              const SizedBox(height: 10),
              Row(
                children: [
                  Switch(
                    value: alarm.isEnabled,
                    activeColor: const Color(0xFF00B4D8),
                    onChanged: (value) {
                      provider.toggleAlarm(alarm.id);
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.more_vert),
                    color: const Color(0xFF999999),
                    onPressed: () => _showAlarmOptions(alarm, provider),
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
    );
  }

  void _navigateToEdit(AlarmModel? alarm) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => AlarmEditScreen(alarm: alarm),
      ),
    );
  }

  void _showAlarmOptions(AlarmModel alarm, AlarmProvider provider) {
    showModalBottomSheet(
      context: context,
      builder: (context) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('编辑'),
              onTap: () {
                Navigator.pop(context);
                _navigateToEdit(alarm);
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text('删除', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                provider.deleteAlarm(alarm.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

5.3 秒表页面

dart 复制代码
// screens/stopwatch_screen.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/stopwatch_provider.dart';

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

  @override
  State<StopwatchScreen> createState() => _StopwatchScreenState();
}

class _StopwatchScreenState extends State<StopwatchScreen> {
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _startTimer();
  }

  void _startTimer() {
    _timer = Timer.periodic(const Duration(milliseconds: 10), (_) {
      if (mounted) {
        context.read<StopwatchProvider>().tick();
      }
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Consumer<StopwatchProvider>(
          builder: (context, provider, child) {
            return Column(
              children: [
                _buildHeader(),
                Expanded(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        provider.formattedTime,
                        style: const TextStyle(
                          fontSize: 72,
                          fontWeight: FontWeight.w300,
                          color: Color(0xFF333333),
                        ),
                      ),
                    ],
                  ),
                ),
                _buildControls(provider),
                if (provider.laps.isNotEmpty) _buildLapList(provider),
              ],
            );
          },
        ),
      ),
    );
  }

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: const [
          Text(
            '秒表',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: Color(0xFF333333),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildControls(StopwatchProvider provider) {
    return Padding(
      padding: const EdgeInsets.all(30),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          if (provider.laps.isNotEmpty || provider.elapsedTime > 0)
            _buildButton(
              label: '重置',
              onPressed: provider.reset,
              bgColor: const Color(0xFFF0F0F0),
              textColor: const Color(0xFF666666),
            ),
          if (provider.isRunning)
            _buildButton(
              label: '计次',
              onPressed: provider.lap,
              bgColor: const Color(0xFFE0F7FA),
              textColor: const Color(0xFF00B4D8),
            ),
          _buildButton(
            label: provider.isRunning ? '暂停' : '开始',
            onPressed: provider.isRunning ? provider.pause : provider.start,
            bgColor: const Color(0xFF00B4D8),
            textColor: Colors.white,
          ),
        ],
      ),
    );
  }

  Widget _buildButton({
    required String label,
    required VoidCallback onPressed,
    required Color bgColor,
    required Color textColor,
  }) {
    return SizedBox(
      width: 100,
      height: 60,
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: bgColor,
          foregroundColor: textColor,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(30),
          ),
        ),
        child: Text(
          label,
          style: const TextStyle(fontSize: 18),
        ),
      ),
    );
  }

  Widget _buildLapList(StopwatchProvider provider) {
    return Container(
      height: 200,
      margin: const EdgeInsets.symmetric(horizontal: 16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 4,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            decoration: const BoxDecoration(
              color: Color(0xFFF5F5F5),
              borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
            ),
            child: const Row(
              children: [
                Text('计次', style: TextStyle(color: Color(0xFF999999))),
                SizedBox(width: 100),
                Text('计时', style: TextStyle(color: Color(0xFF999999))),
                Spacer(),
                Text('计次时间', style: TextStyle(color: Color(0xFF999999))),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              reverse: true,
              itemCount: provider.laps.length,
              itemBuilder: (context, index) {
                final reversedIndex = provider.laps.length - 1 - index;
                final lap = provider.laps[reversedIndex];
                return Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 20,
                    vertical: 15,
                  ),
                  decoration: const BoxDecoration(
                    border: Border(
                      bottom: BorderSide(color: Color(0xFFF0F0F0)),
                    ),
                  ),
                  child: Row(
                    children: [
                      Text(
                        '第${lap.lapNumber}次',
                        style: const TextStyle(
                          fontSize: 16,
                          color: Color(0xFF333333),
                        ),
                      ),
                      const SizedBox(width: 40),
                      Text(
                        provider.formatLapTime(lap.elapsedTime),
                        style: const TextStyle(
                          fontSize: 16,
                          color: Color(0xFF333333),
                        ),
                      ),
                      const Spacer(),
                      Text(
                        provider.formatLapTime(lap.lapTime),
                        style: const TextStyle(
                          fontSize: 16,
                          color: Color(0xFF00B4D8),
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

六、运行截图展示

6.1 应用运行界面

以下是闹钟时钟应用在鸿蒙设备上的运行截图:

图1:闹钟主界面

显示当前时间、日期以及已设置的闹钟列表,支持开关控制快速启用/禁用。

图2:添加闹钟界面

通过上下滑动调节时间,支持设置重复周期(单次、每天、工作日、周末、自定义)。

图3:世界时钟界面

展示多个城市的时间,点击可添加更多城市,支持查看不同时区的当前时间。

图4:秒表界面

实时计时显示,支持计次功能,可记录多个分段成绩。

图5:睡眠记录界面

记录睡眠时长和质量评分,生成周统计图表。


七、代码托管

本文涉及的完整代码已托管至AtomGit平台:

仓库地址https://atomgit.com/maaath/flutter_clock_app

仓库结构说明:

复制代码
flutter_clock_app/
├── lib/
│   ├── main.dart
│   ├── models/
│   ├── providers/
│   ├── screens/
│   ├── services/
│   └── widgets/
├── screenshots/     # 运行截图
├── README.md        # 项目说明
└── pubspec.yaml     # 依赖配置

八、总结与展望

通过本文的实战演练,我们完整地学习了如何使用Flutter for OpenHarmony开发一个功能丰富的闹钟时钟应用。项目的核心要点包括:

  1. 模块化架构设计:合理的项目结构有助于代码维护和团队协作
  2. 状态管理:采用Provider模式实现应用状态的统一管理
  3. 本地存储:使用SharedPreferences实现数据的持久化
  4. 跨平台适配:Flutter的跨平台特性使得代码可以在鸿蒙设备上无缝运行

Flutter for OpenHarmony的生态正在快速发展,越来越多的Flutter插件已经完成鸿蒙化适配。未来,我们可以进一步扩展应用功能,如:

  • 添加云同步功能
  • 集成语音提醒
  • 支持表盘样式自定义
  • 增加健康数据联动

期待更多开发者加入Flutter for OpenHarmony的行列,共同推动跨平台技术的发展!


参考资料


相关推荐
maaath3 小时前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
程序猿追3 小时前
从零打造一个“跳一跳”:在HarmonyOS模拟器上用Canvas复刻经典
华为·harmonyos
@不误正业3 小时前
第13章-开源鸿蒙是否适合做端侧AI操作系统
人工智能·开源·harmonyos
UnicornDev3 小时前
【HarmonyOS 6】底部悬浮导航的迷你栏适配(API23)
华为·harmonyos·arkts·鸿蒙
@不误正业4 小时前
OpenHarmony-A2A协议实战-多智能体跨应用协同架构与实现
人工智能·架构·harmonyos·开源鸿蒙
空中海4 小时前
iOS 静态逆向、IPA 结构与 Mach-O 分析
ios·华为·harmonyos
李李李勃谦4 小时前
鸿蒙PC文件加密工具实战:AES-256-GCM 加密
华为·harmonyos
maaath4 小时前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos
千码君20164 小时前
flutter:与Android Studio模拟器的调试分享
android·flutter