Flutter实战:打造功能完整的Todo待办清单应用

前言

Todo待办清单是最经典的应用之一,看似简单却包含了丰富的功能和交互设计。本文将带你从零开始,使用Flutter开发一个功能完整、体验优秀的Todo应用。

应用特色

  • 完整的CRUD操作:创建、读取、更新、删除待办事项
  • 🎯 三级优先级:低、中、高优先级标识
  • 📅 截止日期:设置和提醒逾期任务
  • 🏷️ 分类管理:自定义分类标签
  • 🔍 多维度筛选:按状态、分类筛选
  • 📊 统计分析:完成率、逾期数等统计
  • 💾 数据持久化:使用SharedPreferences本地存储
  • 🎨 Material 3设计:现代化UI设计
  • 👆 滑动删除:Dismissible手势交互
  • 🌓 深色模式:自动适配系统主题

效果展示

在这里插入图片描述
在这里插入图片描述

Todo待办清单

核心功能

待办管理

新建待办

编辑待办

完成标记

删除待办

属性设置

标题描述

优先级

截止日期

分类标签

筛选功能

状态筛选

全部

未完成

已完成

分类筛选

统计功能

完成率

逾期数

优先级分布

分类统计

数据模型设计

1. Todo项模型

class TodoItem {
  String id;                    // 唯一标识
  String title;                 // 标题
  String? description;          // 描述(可选)
  bool isCompleted;             // 是否完成
  DateTime createdAt;           // 创建时间
  DateTime? dueDate;            // 截止日期(可选)
  TodoPriority priority;        // 优先级
  String? category;             // 分类(可选)

  TodoItem({
    required this.id,
    required this.title,
    this.description,
    this.isCompleted = false,
    required this.createdAt,
    this.dueDate,
    this.priority = TodoPriority.medium,
    this.category,
  });

  // JSON序列化
  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'description': description,
    'isCompleted': isCompleted,
    'createdAt': createdAt.toIso8601String(),
    'dueDate': dueDate?.toIso8601String(),
    'priority': priority.index,
    'category': category,
  };

  // JSON反序列化
  factory TodoItem.fromJson(Map<String, dynamic> json) => TodoItem(
    id: json['id'],
    title: json['title'],
    description: json['description'],
    isCompleted: json['isCompleted'],
    createdAt: DateTime.parse(json['createdAt']),
    dueDate: json['dueDate'] != null 
        ? DateTime.parse(json['dueDate']) 
        : null,
    priority: TodoPriority.values[json['priority'] ?? 1],
    category: json['category'],
  );
}

2. 优先级枚举

enum TodoPriority {
  low,
  medium,
  high;

  String get label {
    switch (this) {
      case TodoPriority.low: return '低';
      case TodoPriority.medium: return '中';
      case TodoPriority.high: return '高';
    }
  }

  Color get color {
    switch (this) {
      case TodoPriority.low: return Colors.green;
      case TodoPriority.medium: return Colors.orange;
      case TodoPriority.high: return Colors.red;
    }
  }

  IconData get icon {
    switch (this) {
      case TodoPriority.low: return Icons.arrow_downward;
      case TodoPriority.medium: return Icons.remove;
      case TodoPriority.high: return Icons.arrow_upward;
    }
  }
}

3. 筛选类型枚举

enum FilterType {
  all,
  active,
  completed;

  String get label {
    switch (this) {
      case FilterType.all: return '全部';
      case FilterType.active: return '未完成';
      case FilterType.completed: return '已完成';
    }
  }
}

核心功能实现

1. 数据持久化

使用SharedPreferences存储Todo列表:

// 加载数据
Future<void> _loadTodos() async {
  final prefs = await SharedPreferences.getInstance();
  final String? todosJson = prefs.getString('todos');

  if (todosJson != null) {
    final List<dynamic> decoded = json.decode(todosJson);
    setState(() {
      _todos = decoded.map((item) => TodoItem.fromJson(item)).toList();
      _updateCategories();
    });
  }
}

// 保存数据
Future<void> _saveTodos() async {
  final prefs = await SharedPreferences.getInstance();
  final String encoded = json.encode(
    _todos.map((t) => t.toJson()).toList()
  );
  await prefs.setString('todos', encoded);
}

2. 智能排序算法

List<TodoItem> get _filteredTodos {
  var filtered = _todos;

  // 按完成状态筛选
  switch (_currentFilter) {
    case FilterType.active:
      filtered = filtered.where((t) => !t.isCompleted).toList();
      break;
    case FilterType.completed:
      filtered = filtered.where((t) => t.isCompleted).toList();
      break;
    case FilterType.all:
      break;
  }

  // 按分类筛选
  if (_selectedCategory != null) {
    filtered = filtered
        .where((t) => t.category == _selectedCategory)
        .toList();
  }

  // 排序:未完成在前,按优先级和创建时间
  filtered.sort((a, b) {
    // 1. 未完成的排在前面
    if (a.isCompleted != b.isCompleted) {
      return a.isCompleted ? 1 : -1;
    }
    // 2. 按优先级降序(高优先级在前)
    if (a.priority != b.priority) {
      return b.priority.index - a.priority.index;
    }
    // 3. 按创建时间降序(新的在前)
    return b.createdAt.compareTo(a.createdAt);
  });

  return filtered;
}

3. 滑动删除功能

使用Dismissible实现滑动删除:

Widget _buildTodoItem(TodoItem todo) {
  return Dismissible(
    key: Key(todo.id),
    background: Container(
      color: Colors.red,
      alignment: Alignment.centerRight,
      padding: const EdgeInsets.only(right: 20),
      child: const Icon(Icons.delete, color: Colors.white),
    ),
    direction: DismissDirection.endToStart,
    onDismissed: (_) => _deleteTodo(todo),
    child: Card(
      // ... Todo项内容
    ),
  );
}

4. 撤销删除功能

void _deleteTodo(TodoItem todo) {
  setState(() {
    _todos.removeWhere((t) => t.id == todo.id);
    _updateCategories();
  });
  _saveTodos();

  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: const Text('已删除'),
      action: SnackBarAction(
        label: '撤销',
        onPressed: () {
          setState(() {
            _todos.add(todo);
            _updateCategories();
          });
          _saveTodos();
        },
      ),
    ),
  );
}

5. 日期格式化

智能显示日期(今天、明天、具体日期):

String _formatDate(DateTime date) {
  final now = DateTime.now();
  final today = DateTime(now.year, now.month, now.day);
  final tomorrow = today.add(const Duration(days: 1));
  final dateOnly = DateTime(date.year, date.month, date.day);

  if (dateOnly == today) {
    return '今天';
  } else if (dateOnly == tomorrow) {
    return '明天';
  } else {
    return '${date.month}/${date.day}';
  }
}

6. 逾期检测

final isOverdue = todo.dueDate != null &&
    !todo.isCompleted &&
    todo.dueDate!.isBefore(DateTime.now());

UI组件设计

1. 筛选栏

Widget _buildFilterBar() {
  return Container(
    padding: const EdgeInsets.symmetric(vertical: 8),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: FilterType.values.map((filter) {
        final isSelected = _currentFilter == filter;
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4),
          child: FilterChip(
            label: Text(filter.label),
            selected: isSelected,
            onSelected: (selected) {
              setState(() => _currentFilter = filter);
            },
          ),
        );
      }).toList(),
    ),
  );
}

2. 统计栏

Widget _buildStatsBar(int activeCount, int completedCount) {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    decoration: BoxDecoration(
      color: Theme.of(context)
          .colorScheme
          .surfaceVariant
          .withOpacity(0.3),
      border: Border(
        bottom: BorderSide(
          color: Theme.of(context).dividerColor,
          width: 1,
        ),
      ),
    ),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        _buildStatItem('未完成', activeCount, Colors.blue),
        _buildStatItem('已完成', completedCount, Colors.green),
        _buildStatItem('总计', _todos.length, Colors.grey),
      ],
    ),
  );
}

3. Todo项卡片

Widget _buildTodoItem(TodoItem todo) {
  final isOverdue = todo.dueDate != null &&
      !todo.isCompleted &&
      todo.dueDate!.isBefore(DateTime.now());

  return Card(
    margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    child: ListTile(
      leading: Checkbox(
        value: todo.isCompleted,
        onChanged: (_) => _toggleTodo(todo),
      ),
      title: Text(
        todo.title,
        style: TextStyle(
          decoration: todo.isCompleted 
              ? TextDecoration.lineThrough 
              : null,
          color: todo.isCompleted ? Colors.grey : null,
        ),
      ),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (todo.description != null && todo.description!.isNotEmpty)
            Text(todo.description!, maxLines: 2),
          Wrap(
            spacing: 8,
            children: [
              // 优先级标签
              _buildChip(
                icon: todo.priority.icon,
                label: todo.priority.label,
                color: todo.priority.color,
              ),
              // 分类标签
              if (todo.category != null)
                _buildChip(
                  icon: Icons.label,
                  label: todo.category!,
                  color: Colors.purple,
                ),
              // 截止日期标签
              if (todo.dueDate != null)
                _buildChip(
                  icon: Icons.calendar_today,
                  label: _formatDate(todo.dueDate!),
                  color: isOverdue ? Colors.red : Colors.blue,
                ),
            ],
          ),
        ],
      ),
      trailing: IconButton(
        icon: const Icon(Icons.edit),
        onPressed: () => _editTodo(todo),
      ),
      onTap: () => _toggleTodo(todo),
    ),
  );
}

4. 标签组件

Widget _buildChip({
  required IconData icon,
  required String label,
  required Color color,
}) {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
    decoration: BoxDecoration(
      color: color.withOpacity(0.1),
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: color.withOpacity(0.3)),
    ),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, size: 12, color: color),
        const SizedBox(width: 4),
        Text(
          label,
          style: TextStyle(
            fontSize: 11,
            color: color,
            fontWeight: FontWeight.w500,
          ),
        ),
      ],
    ),
  );
}

编辑面板实现

1. BottomSheet布局

void _addTodo() {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (context) => TodoEditSheet(
      onSave: (todo) {
        setState(() {
          _todos.add(todo);
          _updateCategories();
        });
        _saveTodos();
      },
      existingCategories: _categories.toList(),
    ),
  );
}

2. 优先级选择器

使用Material 3的SegmentedButton:

SegmentedButton<TodoPriority>(
  segments: TodoPriority.values.map((p) {
    return ButtonSegment(
      value: p,
      label: Text(p.label),
      icon: Icon(p.icon),
    );
  }).toList(),
  selected: {_priority},
  onSelectionChanged: (Set<TodoPriority> selected) {
    setState(() => _priority = selected.first);
  },
)

3. 日期选择器

Future<void> _selectDate() async {
  final date = await showDatePicker(
    context: context,
    initialDate: _dueDate ?? DateTime.now(),
    firstDate: DateTime.now(),
    lastDate: DateTime.now().add(const Duration(days: 365)),
  );

  if (date != null) {
    setState(() => _dueDate = date);
  }
}

4. 分类选择

Wrap(
  spacing: 8,
  runSpacing: 8,
  children: [
    ...widget.existingCategories.map((cat) {
      return ChoiceChip(
        label: Text(cat),
        selected: _category == cat,
        onSelected: (selected) {
          setState(() => _category = selected ? cat : null);
        },
      );
    }),
    ActionChip(
      label: const Text('+ 新分类'),
      onPressed: _addNewCategory,
    ),
  ],
)

统计功能实现

void _showStatistics() {
  final total = _todos.length;
  final completed = _todos.where((t) => t.isCompleted).length;
  final active = total - completed;
  final completionRate = total > 0 
      ? (completed / total * 100).toStringAsFixed(1) 
      : '0.0';

  final highPriority = _todos
      .where((t) => !t.isCompleted && t.priority == TodoPriority.high)
      .length;
      
  final overdue = _todos.where((t) {
    return !t.isCompleted &&
        t.dueDate != null &&
        t.dueDate!.isBefore(DateTime.now());
  }).length;

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('📊 统计信息'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _buildStatRow('总待办数', total.toString()),
          _buildStatRow('已完成', completed.toString()),
          _buildStatRow('未完成', active.toString()),
          _buildStatRow('完成率', '$completionRate%'),
          const Divider(),
          _buildStatRow('高优先级', highPriority.toString(), Colors.red),
          _buildStatRow('已逾期', overdue.toString(), Colors.orange),
          const Divider(),
          _buildStatRow('分类数', _categories.length.toString()),
        ],
      ),
    ),
  );
}

技术要点详解

1. SharedPreferences使用

操作 方法 说明
获取实例 SharedPreferences.getInstance() 异步获取
存储字符串 prefs.setString(key, value) 返回Future
读取字符串 prefs.getString(key) 返回String?
删除数据 prefs.remove(key) 删除指定key
清空数据 prefs.clear() 清空所有数据

2. JSON序列化最佳实践

// 1. 定义toJson方法
Map<String, dynamic> toJson() => {
  'id': id,
  'title': title,
  // 处理可空字段
  'description': description,
  // 处理DateTime
  'createdAt': createdAt.toIso8601String(),
  // 处理枚举
  'priority': priority.index,
};

// 2. 定义fromJson工厂构造函数
factory TodoItem.fromJson(Map<String, dynamic> json) => TodoItem(
  id: json['id'],
  title: json['title'],
  description: json['description'],
  createdAt: DateTime.parse(json['createdAt']),
  // 处理可空DateTime
  dueDate: json['dueDate'] != null 
      ? DateTime.parse(json['dueDate']) 
      : null,
  // 处理枚举,提供默认值
  priority: TodoPriority.values[json['priority'] ?? 1],
);

3. Dismissible手势详解

Dismissible(
  key: Key(todo.id),              // 必须提供唯一key
  direction: DismissDirection.endToStart,  // 只允许从右向左滑动
  background: Container(          // 滑动时显示的背景
    color: Colors.red,
    alignment: Alignment.centerRight,
    child: Icon(Icons.delete),
  ),
  onDismissed: (direction) {      // 滑动完成回调
    _deleteTodo(todo);
  },
  confirmDismiss: (direction) async {  // 可选:确认对话框
    return await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('确认删除?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: Text('删除'),
          ),
        ],
      ),
    );
  },
  child: ListTile(...),
)

4. ModalBottomSheet键盘适配

showModalBottomSheet(
  context: context,
  isScrollControlled: true,  // 允许全屏高度
  builder: (context) => Padding(
    padding: EdgeInsets.only(
      // 关键:根据键盘高度调整padding
      bottom: MediaQuery.of(context).viewInsets.bottom,
      left: 16,
      right: 16,
      top: 16,
    ),
    child: SingleChildScrollView(  // 允许滚动
      child: Column(...),
    ),
  ),
)

功能扩展建议

1. 子任务功能

class TodoItem {
  // ... 现有字段
  List<SubTask>? subTasks;
}

class SubTask {
  String id;
  String title;
  bool isCompleted;
}

2. 重复任务

enum RepeatType {
  none,
  daily,
  weekly,
  monthly;
}

class TodoItem {
  // ... 现有字段
  RepeatType repeatType;
  DateTime? repeatEndDate;
}

3. 提醒通知

使用flutter_local_notifications包:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

Future<void> _scheduleNotification(TodoItem todo) async {
  if (todo.dueDate == null) return;
  
  final flutterLocalNotificationsPlugin = 
      FlutterLocalNotificationsPlugin();
  
  await flutterLocalNotificationsPlugin.zonedSchedule(
    todo.id.hashCode,
    '待办提醒',
    todo.title,
    tz.TZDateTime.from(todo.dueDate!, tz.local),
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'todo_channel',
        'Todo提醒',
        importance: Importance.high,
      ),
    ),
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
  );
}

4. 搜索功能

List<TodoItem> _searchTodos(String query) {
  if (query.isEmpty) return _todos;
  
  return _todos.where((todo) {
    return todo.title.toLowerCase().contains(query.toLowerCase()) ||
        (todo.description?.toLowerCase().contains(query.toLowerCase()) 
            ?? false);
  }).toList();
}

5. 数据导出

import 'package:share_plus/share_plus.dart';

Future<void> _exportTodos() async {
  final jsonString = json.encode(
    _todos.map((t) => t.toJson()).toList()
  );
  
  final file = File('${(await getTemporaryDirectory()).path}/todos.json');
  await file.writeAsString(jsonString);
  
  await Share.shareXFiles([XFile(file.path)]);
}

6. 云同步

使用Firebase Firestore:

import 'package:cloud_firestore/cloud_firestore.dart';

class TodoService {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final String userId;

  TodoService(this.userId);

  // 同步到云端
  Future<void> syncTodo(TodoItem todo) async {
    await _firestore
        .collection('users')
        .doc(userId)
        .collection('todos')
        .doc(todo.id)
        .set(todo.toJson());
  }

  // 从云端加载
  Stream<List<TodoItem>> getTodos() {
    return _firestore
        .collection('users')
        .doc(userId)
        .collection('todos')
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => TodoItem.fromJson(doc.data()))
            .toList());
  }
}

性能优化建议

1. 列表优化

ListView.builder(
  // 使用builder构造器,只构建可见项
  itemCount: _filteredTodos.length,
  itemBuilder: (context, index) {
    return _buildTodoItem(_filteredTodos[index]);
  },
  // 添加缓存范围
  cacheExtent: 100,
)

2. 防抖保存

避免频繁保存:

Timer? _saveTimer;

void _debouncedSave() {
  _saveTimer?.cancel();
  _saveTimer = Timer(const Duration(milliseconds: 500), () {
    _saveTodos();
  });
}

3. 分页加载

class _TodoListPageState extends State<TodoListPage> {
  int _currentPage = 0;
  final int _pageSize = 20;

  List<TodoItem> get _pagedTodos {
    final start = 0;
    final end = (_currentPage + 1) * _pageSize;
    return _filteredTodos.take(end).toList();
  }

  void _loadMore() {
    if (_pagedTodos.length < _filteredTodos.length) {
      setState(() => _currentPage++);
    }
  }
}

常见问题解答

Q1: 如何实现拖拽排序?

A: 使用ReorderableListView:

ReorderableListView.builder(
  itemCount: _todos.length,
  onReorder: (oldIndex, newIndex) {
    setState(() {
      if (newIndex > oldIndex) newIndex--;
      final item = _todos.removeAt(oldIndex);
      _todos.insert(newIndex, item);
    });
    _saveTodos();
  },
  itemBuilder: (context, index) {
    return _buildTodoItem(_todos[index]);
  },
)

Q2: 如何实现批量操作?

A: 添加选择模式:

bool _isSelectionMode = false;
Set<String> _selectedIds = {};

void _toggleSelection(String id) {
  setState(() {
    if (_selectedIds.contains(id)) {
      _selectedIds.remove(id);
    } else {
      _selectedIds.add(id);
    }
  });
}

void _deleteSelected() {
  setState(() {
    _todos.removeWhere((t) => _selectedIds.contains(t.id));
    _selectedIds.clear();
    _isSelectionMode = false;
  });
  _saveTodos();
}

Q3: 数据量大时如何优化?

A: 使用数据库(如sqflite)替代SharedPreferences:

import 'package:sqflite/sqflite.dart';

class TodoDatabase {
  static Database? _database;

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

  Future<Database> _initDatabase() async {
    return await openDatabase(
      'todos.db',
      version: 1,
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE todos(id TEXT PRIMARY KEY, title TEXT, ...)',
        );
      },
    );
  }
}

项目结构

lib/
├── main.dart                    # 主程序入口
├── models/
│   ├── todo_item.dart          # Todo数据模型
│   ├── todo_priority.dart      # 优先级枚举
│   └── filter_type.dart        # 筛选类型
├── screens/
│   ├── todo_list_page.dart     # 主列表页面
│   └── todo_edit_sheet.dart    # 编辑面板
├── widgets/
│   ├── todo_item_card.dart     # Todo卡片组件
│   ├── filter_bar.dart         # 筛选栏
│   ├── stats_bar.dart          # 统计栏
│   └── priority_chip.dart      # 优先级标签
├── services/
│   └── todo_service.dart       # 数据服务
└── utils/
    ├── date_formatter.dart     # 日期格式化
    └── constants.dart          # 常量定义

总结

本文实现了一个功能完整的Todo待办清单应用,涵盖了以下核心技术:

  1. 数据持久化:SharedPreferences + JSON序列化
  2. 状态管理:StatefulWidget + setState
  3. 手势交互:Dismissible滑动删除
  4. Material 3设计:现代化UI组件
  5. 智能排序:多维度排序算法
  6. 统计分析:完成率、逾期检测

通过本项目,你不仅学会了如何实现Todo应用,还掌握了Flutter中数据管理、UI设计、交互优化的核心技术。这些知识可以应用到更多场景,如笔记应用、项目管理、习惯追踪等领域。

待办清单虽小,却是提升效率的利器。希望这个应用能帮助你更好地管理时间,完成目标!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐