时间倒计时都是广告? 俺们自己写 程序要有自己的浪漫! 👌

131 阅读9分钟

🚀 Flutter倒计时应用项目搭建指南

主要近期想纪念一些日期,发现全是广告,自己做一个吧

本文档将详细介绍如何从零开始搭建"圆时间"Flutter倒计时应用,包括环境配置、项目初始化、核心功能实现等完整流程。

📋 目录

🛠 环境准备

1. 安装Flutter SDK

Windows环境:

# 1. 下载Flutter SDK
# 访问 https://flutter.dev/docs/get-started/install/windows
# 下载最新稳定版本

# 2. 解压到目标目录
# 例如: C:\flutter

# 3. 添加环境变量
# 将 C:\flutter\bin 添加到 PATH

# 4. 验证安装
flutter doctor

macOS环境:

# 使用Homebrew安装
brew install flutter

# 或者手动下载
# 访问 https://flutter.dev/docs/get-started/install/macos

# 验证安装
flutter doctor

2. 配置开发环境

Android Studio设置:

# 1. 下载并安装Android Studio
# 2. 安装Flutter和Dart插件
# 3. 配置Android SDK
flutter doctor --android-licenses

VS Code设置:

# 安装必要的扩展
# - Flutter
# - Dart
# - Flutter Widget Snippets

3. 验证环境

# 检查环境配置
flutter doctor -v

# 创建测试项目验证
flutter create test_app
cd test_app
flutter run

🎯 项目初始化

1. 创建项目

# 创建新的Flutter项目
flutter create countdown_app
cd countdown_app

# 检查项目结构
ls -la

2. 配置pubspec.yaml

name: countdown_app
description: "精美的Flutter倒计时应用"
version: 1.0.0+1

environment:
  sdk: '>=3.8.0 <4.0.0'
  flutter: ">=3.32.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  
  # 状态管理
  provider: ^6.1.2
  
  # 数据存储
  sqflite: ^2.4.1
  shared_preferences: ^2.3.2
  path: ^1.9.0
  
  # UI增强
  google_fonts: ^6.2.1
  flutter_colorpicker: ^1.1.0
  animations: ^2.0.11
  animated_text_kit: ^4.2.2
  
  # 国际化
  intl: ^0.19.0
  
  # 其他工具
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  flutter_launcher_icons: ^0.14.1

flutter:
  uses-material-design: true
  generate: true
  
  assets:
    - assets/icon/
    - assets/images/

3. 安装依赖

flutter pub get

🏗 项目架构设计

1. 文件夹结构

lib/
├── main.dart                 # 应用入口
├── l10n/                     # 国际化文件
│   ├── app_en.arb
│   ├── app_zh.arb
│   └── app_localizations.dart
├── models/                   # 数据模型
│   └── countdown_model.dart
├── providers/                # 状态管理
│   ├── countdown_provider.dart
│   ├── theme_provider.dart
│   └── locale_provider.dart
├── screens/                  # 页面
│   ├── main_screen.dart
│   ├── home_screen.dart
│   ├── add_screen.dart
│   ├── detail_screen.dart
│   ├── discover_screen.dart
│   ├── edit_screen.dart
│   └── settings_screen.dart
├── widgets/                  # 通用组件
│   ├── countdown_card.dart
│   └── chinese_date_picker.dart
└── services/                 # 服务层
    ├── database_service.dart
    └── export_service.dart

2. 创建基础文件夹

mkdir -p lib/{models,providers,screens,widgets,services,l10n}

💻 核心功能实现

1. 数据模型设计

创建 lib/models/countdown_model.dart:

class CountdownModel {
  final int? id;
  final String title;
  final String description;
  final DateTime targetDate;
  final DateTime createdAt;
  final String eventType;
  final int themeIndex;
  final bool isCompleted;

  CountdownModel({
    this.id,
    required this.title,
    required this.description,
    required this.targetDate,
    required this.createdAt,
    required this.eventType,
    this.themeIndex = 0,
    this.isCompleted = false,
  });

  // 计算剩余时间
  Duration get remainingDuration {
    final now = DateTime.now();
    if (targetDate.isBefore(now)) {
      return Duration.zero;
    }
    return targetDate.difference(now);
  }

  // 是否已过期
  bool get isExpired => remainingDuration == Duration.zero;

  // JSON序列化
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'description': description,
      'targetDate': targetDate.toIso8601String(),
      'createdAt': createdAt.toIso8601String(),
      'eventType': eventType,
      'themeIndex': themeIndex,
      'isCompleted': isCompleted ? 1 : 0,
    };
  }

  factory CountdownModel.fromJson(Map<String, dynamic> json) {
    return CountdownModel(
      id: json['id'],
      title: json['title'],
      description: json['description'],
      targetDate: DateTime.parse(json['targetDate']),
      createdAt: DateTime.parse(json['createdAt']),
      eventType: json['eventType'],
      themeIndex: json['themeIndex'] ?? 0,
      isCompleted: json['isCompleted'] == 1,
    );
  }

  CountdownModel copyWith({
    int? id,
    String? title,
    String? description,
    DateTime? targetDate,
    DateTime? createdAt,
    String? eventType,
    int? themeIndex,
    bool? isCompleted,
  }) {
    return CountdownModel(
      id: id ?? this.id,
      title: title ?? this.title,
      description: description ?? this.description,
      targetDate: targetDate ?? this.targetDate,
      createdAt: createdAt ?? this.createdAt,
      eventType: eventType ?? this.eventType,
      themeIndex: themeIndex ?? this.themeIndex,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}

2. 数据库服务

创建 lib/services/database_service.dart:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/countdown_model.dart';

class DatabaseService {
  static final DatabaseService _instance = DatabaseService._internal();
  factory DatabaseService() => _instance;
  DatabaseService._internal();

  static Database? _database;

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'countdown.db');

    return await openDatabase(
      path,
      version: 1,
      onCreate: _createTables,
    );
  }

  Future<void> _createTables(Database db, int version) async {
    await db.execute('''
      CREATE TABLE countdowns (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        description TEXT NOT NULL,
        targetDate TEXT NOT NULL,
        createdAt TEXT NOT NULL,
        eventType TEXT NOT NULL,
        themeIndex INTEGER DEFAULT 0,
        isCompleted INTEGER DEFAULT 0
      )
    ''');
  }

  // 插入倒计时
  Future<int> insertCountdown(CountdownModel countdown) async {
    final db = await database;
    return await db.insert('countdowns', countdown.toJson());
  }

  // 获取所有倒计时
  Future<List<CountdownModel>> getAllCountdowns() async {
    final db = await database;
    final maps = await db.query('countdowns', orderBy: 'targetDate ASC');
    return maps.map((map) => CountdownModel.fromJson(map)).toList();
  }

  // 更新倒计时
  Future<int> updateCountdown(CountdownModel countdown) async {
    final db = await database;
    return await db.update(
      'countdowns',
      countdown.toJson(),
      where: 'id = ?',
      whereArgs: [countdown.id],
    );
  }

  // 删除倒计时
  Future<int> deleteCountdown(int id) async {
    final db = await database;
    return await db.delete(
      'countdowns',
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // 根据类型查询
  Future<List<CountdownModel>> getCountdownsByType(String eventType) async {
    final db = await database;
    final maps = await db.query(
      'countdowns',
      where: 'eventType = ?',
      whereArgs: [eventType],
      orderBy: 'targetDate ASC',
    );
    return maps.map((map) => CountdownModel.fromJson(map)).toList();
  }
}

3. 状态管理

创建 lib/providers/countdown_provider.dart:

import 'package:flutter/material.dart';
import '../models/countdown_model.dart';
import '../services/database_service.dart';

class CountdownProvider with ChangeNotifier {
  final DatabaseService _dbService = DatabaseService();
  List<CountdownModel> _countdowns = [];
  bool _isLoading = false;

  List<CountdownModel> get countdowns => _countdowns;
  bool get isLoading => _isLoading;

  // 获取未过期的倒计时
  List<CountdownModel> get activeCountdowns =>
      _countdowns.where((countdown) => !countdown.isExpired).toList();

  // 获取已过期的倒计时
  List<CountdownModel> get expiredCountdowns =>
      _countdowns.where((countdown) => countdown.isExpired).toList();

  // 加载所有倒计时
  Future<void> loadCountdowns() async {
    _isLoading = true;
    notifyListeners();

    try {
      _countdowns = await _dbService.getAllCountdowns();
    } catch (e) {
      debugPrint('Error loading countdowns: $e');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  // 添加倒计时
  Future<void> addCountdown(CountdownModel countdown) async {
    try {
      final id = await _dbService.insertCountdown(countdown);
      final newCountdown = countdown.copyWith(id: id);
      _countdowns.add(newCountdown);
      _sortCountdowns();
      notifyListeners();
    } catch (e) {
      debugPrint('Error adding countdown: $e');
      rethrow;
    }
  }

  // 更新倒计时
  Future<void> updateCountdown(CountdownModel countdown) async {
    try {
      await _dbService.updateCountdown(countdown);
      final index = _countdowns.indexWhere((c) => c.id == countdown.id);
      if (index != -1) {
        _countdowns[index] = countdown;
        _sortCountdowns();
        notifyListeners();
      }
    } catch (e) {
      debugPrint('Error updating countdown: $e');
      rethrow;
    }
  }

  // 删除倒计时
  Future<void> deleteCountdown(int id) async {
    try {
      await _dbService.deleteCountdown(id);
      _countdowns.removeWhere((countdown) => countdown.id == id);
      notifyListeners();
    } catch (e) {
      debugPrint('Error deleting countdown: $e');
      rethrow;
    }
  }

  // 按日期排序
  void _sortCountdowns() {
    _countdowns.sort((a, b) => a.targetDate.compareTo(b.targetDate));
  }

  // 根据类型筛选
  List<CountdownModel> getCountdownsByType(String eventType) {
    return _countdowns.where((c) => c.eventType == eventType).toList();
  }
}

🎨 UI界面设计

1. 主题管理

创建 lib/providers/theme_provider.dart:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThemeProvider with ChangeNotifier {
  bool _isDarkMode = false;
  int _currentThemeIndex = 0;

  bool get isDarkMode => _isDarkMode;
  int get currentThemeIndex => _currentThemeIndex;

  // 渐变主题色彩
  static const List<List<Color>> gradientThemes = [
    [Color(0xFF667eea), Color(0xFF764ba2)], // 紫蓝渐变
    [Color(0xFFf093fb), Color(0xFFf5576c)], // 粉红渐变
    [Color(0xFF4facfe), Color(0xFF00f2fe)], // 蓝青渐变
    [Color(0xFF43e97b), Color(0xFF38f9d7)], // 绿青渐变
    [Color(0xFFfa709a), Color(0xFFfee140)], // 粉黄渐变
    [Color(0xFFa8edea), Color(0xFFfed6e3)], // 青粉渐变
    [Color(0xFFffecd2), Color(0xFFfcb69f)], // 橙桃渐变
    [Color(0xFFd299c2), Color(0xFFfef9d7)], // 紫米渐变
    [Color(0xFF89f7fe), Color(0xFF66a6ff)], // 青蓝渐变
    [Color(0xFFfdbb2d), Color(0xFF22c1c3)], // 黄青渐变
  ];

  LinearGradient get currentGradient {
    final colors = gradientThemes[_currentThemeIndex];
    return LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: colors,
    );
  }

  ThemeData get lightTheme {
    return ThemeData(
      useMaterial3: true,
      brightness: Brightness.light,
      colorScheme: ColorScheme.fromSeed(
        seedColor: gradientThemes[_currentThemeIndex][0],
        brightness: Brightness.light,
      ),
      appBarTheme: const AppBarTheme(
        elevation: 0,
        centerTitle: true,
      ),
    );
  }

  ThemeData get darkTheme {
    return ThemeData(
      useMaterial3: true,
      brightness: Brightness.dark,
      colorScheme: ColorScheme.fromSeed(
        seedColor: gradientThemes[_currentThemeIndex][0],
        brightness: Brightness.dark,
      ),
      appBarTheme: const AppBarTheme(
        elevation: 0,
        centerTitle: true,
      ),
    );
  }

  Future<void> toggleTheme() async {
    _isDarkMode = !_isDarkMode;
    await _savePreferences();
    notifyListeners();
  }

  Future<void> setThemeIndex(int index) async {
    if (index >= 0 && index < gradientThemes.length) {
      _currentThemeIndex = index;
      await _savePreferences();
      notifyListeners();
    }
  }

  Future<void> loadPreferences() async {
    final prefs = await SharedPreferences.getInstance();
    _isDarkMode = prefs.getBool('isDarkMode') ?? false;
    _currentThemeIndex = prefs.getInt('themeIndex') ?? 0;
    notifyListeners();
  }

  Future<void> _savePreferences() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isDarkMode', _isDarkMode);
    await prefs.setInt('themeIndex', _currentThemeIndex);
  }
}

2. 圆形进度组件

创建 lib/widgets/countdown_card.dart:

import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../models/countdown_model.dart';

class CountdownCard extends StatefulWidget {
  final CountdownModel countdown;
  final VoidCallback? onTap;

  const CountdownCard({
    Key? key,
    required this.countdown,
    this.onTap,
  }) : super(key: key);

  @override
  State<CountdownCard> createState() => _CountdownCardState();
}

class _CountdownCardState extends State<CountdownCard>
    with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 200),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(
      begin: 1.0,
      end: 0.95,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => _animationController.forward(),
      onTapUp: (_) => _animationController.reverse(),
      onTapCancel: () => _animationController.reverse(),
      onTap: widget.onTap,
      child: AnimatedBuilder(
        animation: _scaleAnimation,
        builder: (context, child) {
          return Transform.scale(
            scale: _scaleAnimation.value,
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(20),
                gradient: _getGradient(),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.1),
                    blurRadius: 10,
                    offset: const Offset(0, 5),
                  ),
                ],
              ),
              child: Column(
                children: [
                  _buildHeader(),
                  const SizedBox(height: 20),
                  _buildCircularProgress(),
                  const SizedBox(height: 20),
                  _buildTimeDisplay(),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _buildHeader() {
    return Row(
      children: [
        _buildEventIcon(),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                widget.countdown.title,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              if (widget.countdown.description.isNotEmpty) ...[
                const SizedBox(height: 4),
                Text(
                  widget.countdown.description,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.white.withOpacity(0.8),
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildEventIcon() {
    IconData iconData;
    switch (widget.countdown.eventType) {
      case '生日':
        iconData = Icons.cake;
        break;
      case '纪念日':
        iconData = Icons.favorite;
        break;
      case '节日':
        iconData = Icons.celebration;
        break;
      default:
        iconData = Icons.event;
    }

    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.2),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Icon(
        iconData,
        color: Colors.white,
        size: 24,
      ),
    );
  }

  Widget _buildCircularProgress() {
    final now = DateTime.now();
    final total = widget.countdown.targetDate.difference(widget.countdown.createdAt);
    final remaining = widget.countdown.targetDate.difference(now);
    
    double progress = 0.0;
    if (total.inSeconds > 0) {
      progress = math.max(0.0, math.min(1.0, 
        (total.inSeconds - remaining.inSeconds) / total.inSeconds));
    }

    return SizedBox(
      width: 120,
      height: 120,
      child: CustomPaint(
        painter: CircularProgressPainter(
          progress: widget.countdown.isExpired ? 1.0 : progress,
          backgroundColor: Colors.white.withOpacity(0.3),
          progressColor: Colors.white,
          strokeWidth: 8,
        ),
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                widget.countdown.isExpired ? '已结束' : '剩余',
                style: TextStyle(
                  fontSize: 12,
                  color: Colors.white.withOpacity(0.8),
                ),
              ),
              const SizedBox(height: 4),
              Text(
                widget.countdown.isExpired 
                  ? '00:00:00'
                  : _formatDuration(widget.countdown.remainingDuration),
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildTimeDisplay() {
    final remaining = widget.countdown.remainingDuration;
    
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildTimeUnit('天', remaining.inDays.toString()),
        _buildTimeUnit('时', (remaining.inHours % 24).toString()),
        _buildTimeUnit('分', (remaining.inMinutes % 60).toString()),
        _buildTimeUnit('秒', (remaining.inSeconds % 60).toString()),
      ],
    );
  }

  Widget _buildTimeUnit(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
        Text(
          label,
          style: TextStyle(
            fontSize: 12,
            color: Colors.white.withOpacity(0.8),
          ),
        ),
      ],
    );
  }

  LinearGradient _getGradient() {
    // 这里可以根据主题索引返回不同的渐变
    final gradients = [
      [const Color(0xFF667eea), const Color(0xFF764ba2)],
      [const Color(0xFFf093fb), const Color(0xFFf5576c)],
      [const Color(0xFF4facfe), const Color(0xFF00f2fe)],
      // ... 更多渐变
    ];
    
    final colors = gradients[widget.countdown.themeIndex % gradients.length];
    return LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: colors,
    );
  }

  String _formatDuration(Duration duration) {
    final hours = duration.inHours.toString().padLeft(2, '0');
    final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
    final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
    return '$hours:$minutes:$seconds';
  }
}

class CircularProgressPainter extends CustomPainter {
  final double progress;
  final Color backgroundColor;
  final Color progressColor;
  final double strokeWidth;

  CircularProgressPainter({
    required this.progress,
    required this.backgroundColor,
    required this.progressColor,
    required this.strokeWidth,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;

    // 绘制背景圆环
    final backgroundPaint = Paint()
      ..color = backgroundColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawCircle(center, radius, backgroundPaint);

    // 绘制进度圆环
    final progressPaint = Paint()
      ..color = progressColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final sweepAngle = 2 * math.pi * progress;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -math.pi / 2, // 从顶部开始
      sweepAngle,
      false,
      progressPaint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}

📱 图标设计

1. 创建图标生成器

创建 icon_generator.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>应用图标生成器</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
        }
        .container {
            background: white;
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        canvas {
            border: 2px solid #eee;
            border-radius: 15px;
            margin: 20px auto;
            display: block;
            box-shadow: 0 10px 20px rgba(0,0,0,0.1);
        }
        .controls {
            text-align: center;
            margin: 20px 0;
        }
        button {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 25px;
            cursor: pointer;
            font-size: 16px;
            margin: 0 10px;
            transition: transform 0.2s;
        }
        button:hover {
            transform: translateY(-2px);
        }
        .preview {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin: 20px 0;
        }
        .size-label {
            text-align: center;
            margin-top: 5px;
            font-size: 12px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🕐 圆时间 - 应用图标生成器</h1>
        
        <canvas id="iconCanvas" width="1024" height="1024"></canvas>
        
        <div class="controls">
            <button onclick="generateIcon()">🎨 重新生成图标</button>
            <button onclick="downloadAllSizes()">📦 生成所有尺寸</button>
        </div>
        
        <div class="preview" id="preview"></div>
    </div>

    <script>
        function generateIcon() {
            const canvas = document.getElementById('iconCanvas');
            const ctx = canvas.getContext('2d');
            
            // 清空画布
            ctx.clearRect(0, 0, 1024, 1024);
            
            // 绘制渐变背景
            const gradient = ctx.createLinearGradient(0, 0, 1024, 1024);
            gradient.addColorStop(0, '#667eea');
            gradient.addColorStop(1, '#764ba2');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, 1024, 1024);
            
            // 绘制主圆环
            const centerX = 512;
            const centerY = 512;
            const radius = 300;
            
            // 外圆环背景
            ctx.beginPath();
            ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
            ctx.lineWidth = 40;
            ctx.stroke();
            
            // 进度圆环
            ctx.beginPath();
            ctx.arc(centerX, centerY, radius, -Math.PI/2, Math.PI);
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
            ctx.lineWidth = 40;
            ctx.lineCap = 'round';
            ctx.stroke();
            
            // 绘制时钟指针
            drawClockHands(ctx, centerX, centerY);
            
            // 绘制中心圆
            ctx.beginPath();
            ctx.arc(centerX, centerY, 80, 0, 2 * Math.PI);
            ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
            ctx.fill();
            
            // 绘制时间文字
            ctx.fillStyle = '#764ba2';
            ctx.font = 'bold 60px Arial';
            ctx.textAlign = 'center';
            ctx.fillText('时间', centerX, centerY + 20);
            
            // 生成预览
            generatePreviews();
        }
        
        function drawClockHands(ctx, centerX, centerY) {
            const now = new Date();
            const hours = now.getHours() % 12;
            const minutes = now.getMinutes();
            
            // 时针
            const hourAngle = (hours * 30 + minutes * 0.5) * Math.PI / 180 - Math.PI / 2;
            ctx.beginPath();
            ctx.moveTo(centerX, centerY);
            ctx.lineTo(
                centerX + Math.cos(hourAngle) * 100,
                centerY + Math.sin(hourAngle) * 100
            );
            ctx.strokeStyle = '#764ba2';
            ctx.lineWidth = 12;
            ctx.lineCap = 'round';
            ctx.stroke();
            
            // 分针
            const minuteAngle = minutes * 6 * Math.PI / 180 - Math.PI / 2;
            ctx.beginPath();
            ctx.moveTo(centerX, centerY);
            ctx.lineTo(
                centerX + Math.cos(minuteAngle) * 140,
                centerY + Math.sin(minuteAngle) * 140
            );
            ctx.strokeStyle = '#667eea';
            ctx.lineWidth = 8;
            ctx.lineCap = 'round';
            ctx.stroke();
        }
        
        function generatePreviews() {
            const sizes = [192, 144, 96, 72, 48];
            const preview = document.getElementById('preview');
            preview.innerHTML = '';
            
            sizes.forEach(size => {
                const previewCanvas = document.createElement('canvas');
                previewCanvas.width = size;
                previewCanvas.height = size;
                previewCanvas.style.border = '1px solid #ddd';
                previewCanvas.style.borderRadius = '8px';
                
                const previewCtx = previewCanvas.getContext('2d');
                const mainCanvas = document.getElementById('iconCanvas');
                previewCtx.drawImage(mainCanvas, 0, 0, size, size);
                
                const container = document.createElement('div');
                container.appendChild(previewCanvas);
                
                const label = document.createElement('div');
                label.className = 'size-label';
                label.textContent = `${size}×${size}`;
                container.appendChild(label);
                
                preview.appendChild(container);
            });
        }
        
        function downloadAllSizes() {
            const sizes = [
                {name: 'app_icon', size: 1024},
                {name: 'app_icon_foreground', size: 1024}
            ];
            
            sizes.forEach(item => {
                const canvas = document.createElement('canvas');
                canvas.width = item.size;
                canvas.height = item.size;
                const ctx = canvas.getContext('2d');
                
                const mainCanvas = document.getElementById('iconCanvas');
                ctx.drawImage(mainCanvas, 0, 0, item.size, item.size);
                
                // 下载
                const link = document.createElement('a');
                link.download = `${item.name}.png`;
                link.href = canvas.toDataURL();
                link.click();
            });
        }
        
        // 页面加载时生成图标
        window.onload = generateIcon;
    </script>
</body>
</html>

2. 配置图标生成器

创建 flutter_launcher_icons.yaml 配置:

flutter_launcher_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/app_icon.png"
  adaptive_icon_background: "#667eea"
  adaptive_icon_foreground: "assets/icon/app_icon_foreground.png"
  
  # 移除不必要的默认图标
  remove_alpha_ios: true
  
  # Web图标
  web:
    generate: true
    image_path: "assets/icon/app_icon.png"
  
  # Windows图标
  windows:
    generate: true
    image_path: "assets/icon/app_icon.png"
    icon_size: 48
  
  # macOS图标
  macos:
    generate: true
    image_path: "assets/icon/app_icon.png"

3. 生成图标命令

# 生成应用图标
flutter pub run flutter_launcher_icons:main

# 如果有错误,先清理再重新生成
flutter clean
flutter pub get
flutter pub run flutter_launcher_icons:main

📦 构建发布

1. Android APK构建

配置签名 (可选):

# 生成签名密钥
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload

# 创建 android/key.properties
echo "storePassword=your_password
keyPassword=your_password
keyAlias=upload
storeFile=../upload-keystore.jks" > android/key.properties

修改 android/app/build.gradle:

// 在 android 块前添加
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    compileSdkVersion flutter.compileSdkVersion
    
    defaultConfig {
        applicationId "com.daojish.countdown_app"
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
    
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

构建APK:

# 构建debug版本
flutter build apk --debug

# 构建release版本
flutter build apk --release

# 构建App Bundle (推荐用于Google Play)
flutter build appbundle --release

# 分平台构建
flutter build apk --split-per-abi --release

2. iOS构建 (需要macOS)

# 构建iOS应用
flutter build ios --release

# 生成IPA文件 (需要Xcode)
# 在Xcode中打开 ios/Runner.xcworkspace
# Product -> Archive -> Distribute App

3. Web构建

# 构建Web应用
flutter build web --release

# 本地服务器测试
cd build/web
python -m http.server 8000

⚠️ 常见问题

1. 环境问题

# Flutter doctor检查
flutter doctor -v

# 清理项目
flutter clean
flutter pub get

# 升级Flutter
flutter upgrade

2. 依赖问题

# 清理pub缓存
flutter pub cache repair

# 重新获取依赖
rm pubspec.lock
flutter pub get

3. 构建问题

# Android构建问题
cd android
./gradlew clean

# iOS构建问题
cd ios
rm -rf Pods
rm Podfile.lock
pod install

4. 性能优化

// 使用const构造函数
const Text('Hello World')

// 避免不必要的build方法调用
class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return const Text('Optimized Widget');
  }
}

// 使用ListView.builder代替ListView
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemWidget(items[index]),
)

🎯 项目特色总结

通过这个项目,你将学到:

🛠 技术技能

  • Flutter开发 - 完整的移动应用开发流程
  • 状态管理 - Provider模式的实际应用
  • 数据库操作 - SQLite本地数据存储
  • 自定义绘制 - Canvas API的使用
  • 动画效果 - Flutter动画系统

🎨 设计技能

  • UI/UX设计 - Material Design 3规范
  • 颜色搭配 - 渐变色彩系统设计
  • 图标设计 - 应用图标的设计和生成
  • 动效设计 - 流畅的交互动画

📱 产品技能

  • 需求分析 - 倒计时应用的功能规划
  • 用户体验 - 简洁直观的界面设计
  • 性能优化 - 应用性能和体验优化
  • 发布流程 - 完整的应用发布流程

这个项目不仅是一个功能完整的应用,更是学习Flutter开发的优秀实践案例。希望这份详细的搭建指南能帮助您快速掌握Flutter开发技能!