flutter学习第 16 节:项目实战:综合应用开发(上)

今天我们将通过一个待办清单 App 的实战开发,综合运用 Flutter 的核心知识。待办清单作为经典的入门项目,涵盖了页面跳转、数据展示、基础交互等多个关键知识点,非常适合用来巩固前期学习的内容。

一、需求分析与页面设计

1. 核心需求拆解

待办清单 App 的核心目标是帮助用户管理日常任务,我们需要实现的核心功能包括:

  • 展示所有待办任务列表(已完成 / 未完成分类)
  • 查看单个任务的详细信息
  • 添加新任务
  • 编辑现有任务
  • 标记任务完成状态
  • 删除任务
  • 基本设置功能(如主题切换开关、关于页面等)

2. 页面架构设计

基于上述需求,我们将设计三个核心页面:

  • 首页(任务列表页) :作为 App 的入口,顶部显示标题和添加按钮,中间区域用列表展示所有任务,支持下拉刷新和左滑删除操作。列表项需显示任务标题、创建时间和完成状态标记。
  • 详情页:点击列表项进入,展示任务的完整信息(标题、内容、创建时间、截止时间等),底部提供编辑和删除按钮。
  • 设置页:通过首页的设置图标进入,包含开关组件(如是否开启通知)、关于我们入口、清理缓存按钮等。

3. 原型草图构思

首页采用 "AppBar+ListView" 的经典结构,AppBar 右侧放置 "+" 图标按钮用于添加任务;详情页使用 ScrollView 避免内容溢出,顶部用 AppBar 提供返回按钮;设置页采用 ListView.builder 构建设置项列表,每个设置项由图标、文字和操作组件(开关 / 箭头)组成。


二、项目架构搭建:目录划分

合理的目录结构能让项目更易于维护,我们采用以下划分方式:

1. pages 目录

存放所有完整页面,每个页面单独创建一个文件夹,包含对应的 dart 文件。例如:

  • home_page:首页相关代码
  • detail_page:详情页相关代码
  • setting_page:设置页相关代码

2. widgets 目录

存放可复用的自定义组件,按功能分类创建子目录:

  • common:通用组件(如自定义按钮、输入框)
  • task:与任务相关的组件(如任务列表项、任务状态标签)

3. utils 目录

存放工具类和辅助方法:

  • router.dart:路由管理工具
  • date_utils.dart:日期处理工具
  • storage_utils.dart:本地存储工具(下节课详细实现)

4. models 目录

存放数据模型类,采用 Dart 的类来定义数据结构:

dart 复制代码
// models/task_model.dart
class Task {
  final String id; // 任务唯一标识
  String title; // 任务标题
  String content; // 任务内容
  DateTime createTime; // 创建时间
  DateTime? deadline; // 截止时间(可选)
  bool isCompleted; // 是否完成

  Task({
    required this.id,
    required this.title,
    this.content = '',
    DateTime? createTime,
    this.deadline,
    this.isCompleted = false,
  }) : createTime = createTime ?? DateTime.now();
}

三、核心工具类实现

1. 日期处理工具(utils/date_utils.dart)

dart 复制代码
import 'package:intl/intl.dart';

class DateUtils {
  /// 格式化日期为"yyyy-MM-dd"格式
  static String formatDate(DateTime date) {
    return DateFormat('yyyy-MM-dd').format(date);
  }

  /// 格式化日期为"yyyy-MM-dd HH:mm"格式
  static String formatDateTime(DateTime date) {
    return DateFormat('yyyy-MM-dd HH:mm').format(date);
  }

  /// 格式化日期为友好显示(如"今天 14:30"、"昨天 10:15")
  static String formatFriendly(DateTime date) {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final yesterday = today.subtract(const Duration(days: 1));
    final dateOnly = DateTime(date.year, date.month, date.day);

    if (dateOnly == today) {
      return '今天 ${DateFormat('HH:mm').format(date)}';
    } else if (dateOnly == yesterday) {
      return '昨天 ${DateFormat('HH:mm').format(date)}';
    } else if (date.year == now.year) {
      return DateFormat('MM-dd HH:mm').format(date);
    } else {
      return DateFormat('yyyy-MM-dd HH:mm').format(date);
    }
  }
}

使用前需要在pubspec.yaml中添加依赖:

yaml 复制代码
dependencies:
  intl: ^0.20.2

2. 路由管理工具(utils/router.dart)

dart 复制代码
import 'package:flutter/material.dart';
import '../pages/home_page.dart';
import '../pages/detail_page.dart';
import '../pages/setting_page.dart';

class Router {
  // 路由名称常量
  static const String home = '/home';
  static const String detail = '/detail';
  static const String setting = '/setting';

  // 路由配置
  static Map<String, WidgetBuilder> routes = {
    home: (context) => const HomePage(),
    setting: (context) => const SettingPage(),
    // 详情页需要动态参数,在跳转时处理
  };

  // 生成路由
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case detail:
        final args = settings.arguments as Map<String, dynamic>?;
        return MaterialPageRoute(
          builder: (context) =>
              DetailPage(task: args?['task'], onSave: args?['onSave']),
        );
      default:
        return MaterialPageRoute(
          builder: (context) =>
              const Scaffold(body: Center(child: Text('页面不存在'))),
        );
    }
  }
}

3. 应用入口(main.dart)

dart 复制代码
import 'pages/home_page.dart';
import 'package:flutter/material.dart' hide Router;
import 'utils/router.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '待办清单',
      // 应用主题配置
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        // 配置全局文本样式
        textTheme: const TextTheme(
          bodyLarge: TextStyle(fontSize: 16),
          bodyMedium: TextStyle(fontSize: 14),
          titleLarge: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
      ),
      // 关闭调试模式标识
      debugShowCheckedModeBanner: false,
      // 初始路由
      initialRoute: Router.home,
      // 路由表
      routes: Router.routes,
      // 生成动态路由
      onGenerateRoute: Router.generateRoute,
      // 首页
      home: const HomePage(),
    );
  }
}

四、基础页面实现

1. 首页实现(pages/home_page.dart)

首页是整个 App 的核心入口,我们需要实现任务列表展示、添加任务按钮和设置入口。

dart 复制代码
import 'package:flutter/material.dart';
import '../../widgets/task/task_item.dart';
import '../../models/task_model.dart';
import 'detail_page.dart';
import 'setting_page.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // 模拟任务数据
  final List<Task> _tasks = [
    Task(
      id: '1',
      title: '学习Flutter',
      content: '完成第16课的实战项目',
      deadline: DateTime.now().add(const Duration(days: 1)),
    ),
    Task(id: '2', title: '购买生活用品', content: '牛奶、面包、水果', isCompleted: true),
  ];

  // 跳转到详情页
  void _navigateToDetail({Task? task}) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => DetailPage(
          task: task,
          onSave: (newTask) {
            setState(() {
              if (task == null) {
                // 添加新任务
                _tasks.add(newTask);
              } else {
                // 编辑现有任务
                final index = _tasks.indexWhere((t) => t.id == task.id);
                if (index != -1) {
                  _tasks[index] = newTask;
                }
              }
            });
          },
        ),
      ),
    );
  }

  // 切换任务完成状态
  void _toggleTaskStatus(String id) {
    setState(() {
      final task = _tasks.firstWhere((t) => t.id == id);
      task.isCompleted = !task.isCompleted;
    });
  }

  // 删除任务
  void _deleteTask(String id) {
    setState(() {
      _tasks.removeWhere((t) => t.id == id);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('待办清单'),
        actions: [
          // 设置按钮
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const SettingPage()),
              );
            },
          ),
        ],
      ),
      body: _tasks.isEmpty
          ? const Center(child: Text('暂无任务,添加一个吧!'))
          : ListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: _tasks.length,
              itemBuilder: (context, index) {
                final task = _tasks[index];
                return TaskItem(
                  task: task,
                  onTap: () => _navigateToDetail(task: task),
                  onStatusChanged: _toggleTaskStatus,
                  onDelete: _deleteTask,
                );
              },
            ),
      // 添加任务按钮
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () => _navigateToDetail(),
      ),
    );
  }
}

2. 详情页实现(pages/detail_page.dart)

详情页负责任务的查看、编辑和删除功能,需要实现表单输入和数据回传。

dart 复制代码
import 'package:flutter/material.dart' hide DateUtils;
import '../../models/task_model.dart';
import '../../utils/date_utils.dart';

class DetailPage extends StatefulWidget {
  final Task? task;
  final Function(Task) onSave;

  const DetailPage({super.key, this.task, required this.onSave});

  @override
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  late final TextEditingController _titleController;
  late final TextEditingController _contentController;
  DateTime? _deadline;
  bool _isCompleted = false;

  @override
  void initState() {
    super.initState();
    // 初始化表单数据(编辑模式)
    if (widget.task != null) {
      _titleController = TextEditingController(text: widget.task!.title);
      _contentController = TextEditingController(text: widget.task!.content);
      _deadline = widget.task!.deadline;
      _isCompleted = widget.task!.isCompleted;
    } else {
      // 新增模式
      _titleController = TextEditingController();
      _contentController = TextEditingController();
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  // 保存任务
  void _saveTask() {
    if (_titleController.text.isEmpty) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('请输入任务标题')));
      return;
    }

    final task = Task(
      id: widget.task?.id ?? DateTime.now().microsecondsSinceEpoch.toString(),
      title: _titleController.text,
      content: _contentController.text,
      createTime: widget.task?.createTime ?? DateTime.now(),
      deadline: _deadline,
      isCompleted: _isCompleted,
    );

    widget.onSave(task);
    Navigator.pop(context);
  }

  // 选择截止日期
  Future<void> _selectDate() async {
    final picked = await showDatePicker(
      context: context,
      initialDate: _deadline ?? DateTime.now(),
      firstDate: DateTime.now(),
      lastDate: DateTime.now().add(const Duration(days: 365)),
    );
    if (picked != null) {
      setState(() => _deadline = picked);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.task == null ? '添加任务' : '编辑任务'),
        actions: [
          // 删除按钮(仅编辑模式显示)
          if (widget.task != null)
            IconButton(
              icon: const Icon(Icons.delete, color: Colors.red),
              onPressed: () {
                Navigator.pop(context);
                // 这里可以添加删除确认对话框
              },
            ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: ListView(
          children: [
            // 任务标题输入
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: '任务标题',
                border: OutlineInputBorder(),
              ),
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            // 任务内容输入
            TextField(
              controller: _contentController,
              decoration: const InputDecoration(
                labelText: '任务内容',
                border: OutlineInputBorder(),
              ),
              maxLines: 4,
            ),
            const SizedBox(height: 16),
            // 截止日期选择
            ListTile(
              title: const Text('截止日期'),
              subtitle: Text(
                _deadline != null ? DateUtils.formatDate(_deadline!) : '未设置',
              ),
              trailing: const Icon(Icons.calendar_today),
              onTap: _selectDate,
            ),
            // 完成状态切换
            SwitchListTile(
              title: const Text('完成状态'),
              value: _isCompleted,
              onChanged: (value) => setState(() => _isCompleted = value),
            ),
            const SizedBox(height: 20),
            // 保存按钮
            ElevatedButton(
              onPressed: _saveTask,
              child: const Padding(
                padding: EdgeInsets.all(12),
                child: Text('保存任务'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

3. 设置页实现(pages/setting_page.dart)

设置页提供 App 的基础配置选项,采用列表形式展示各个设置项。

dart 复制代码
import 'package:flutter/material.dart';

class SettingPage extends StatelessWidget {
  const SettingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置')),
      body: ListView(
        children: [
          // 通知设置
          SwitchListTile(
            title: const Text('开启通知提醒'),
            value: true, // 模拟开启状态
            onChanged: (value) {
              // 这里将在状态管理部分实现实际逻辑
            },
          ),
          // 深色模式设置(下节课实现)
          ListTile(
            title: const Text('深色模式'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              // 跳转到深色模式设置页面
            },
          ),
          // 关于我们
          ListTile(
            title: const Text('关于我们'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              showAboutDialog(
                context: context,
                applicationName: '待办清单',
                applicationVersion: '1.0.0',
                children: [
                  const Padding(
                    padding: EdgeInsets.only(top: 16),
                    child: Text('一款简洁高效的任务管理工具,帮助你更好地规划生活和工作。'),
                  ),
                ],
              );
            },
          ),
          // 清理缓存
          ListTile(
            title: const Text('清理缓存'),
            trailing: const Icon(Icons.delete),
            onTap: () {
              ScaffoldMessenger.of(
                context,
              ).showSnackBar(const SnackBar(content: Text('缓存已清理')));
            },
          ),
        ],
      ),
    );
  }
}

五、自定义组件实现(widgets/task/task_item.dart)

实现任务列表项组件,提高代码复用性

dart 复制代码
import 'package:flutter/material.dart' hide DateUtils;
import '../../models/task_model.dart';
import '../../utils/date_utils.dart';

class TaskItem extends StatelessWidget {
  final Task task;
  final VoidCallback onTap;
  final Function(String) onStatusChanged;
  final Function(String) onDelete;

  const TaskItem({
    super.key,
    required this.task,
    required this.onTap,
    required this.onStatusChanged,
    required this.onDelete,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        onTap: onTap,
        leading: Checkbox(
          value: task.isCompleted,
          onChanged: (value) => onStatusChanged(task.id),
        ),
        title: Text(
          task.title,
          style: TextStyle(
            decoration: task.isCompleted ? TextDecoration.lineThrough : null,
            color: task.isCompleted ? Colors.grey : null,
          ),
        ),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            if (task.deadline != null)
              Text(
                '截止:${DateUtils.formatDate(task.deadline!)}',
                style: const TextStyle(fontSize: 12, color: Colors.grey),
              ),
            Text(
              '创建:${DateUtils.formatDate(task.createTime)}',
              style: const TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ],
        ),
        // 左滑删除功能
        trailing: const Icon(Icons.arrow_forward_ios, size: 18),
        onLongPress: () => onDelete(task.id),
      ),
    );
  }
}
相关推荐
Andy_GF10 分钟前
纯血鸿蒙 HarmonyOS Next 调试证书过期解决流程
前端·ios·harmonyos
用户2018792831671 小时前
Dialog不消失之谜——Android窗口系统的"平行宇宙"
android
用户2018792831671 小时前
Dialog 不消失之谜:一场来自系统底层的 "越狱" 行动
android
pengyu2 小时前
【Kotlin系统化精讲:陆】 | 数据类型之类型系统及精髓:安全性与智能设计的哲学
android·kotlin
用户2018792831673 小时前
DecorView添加到Window和直接用WindowManger添加View的差异?
android
开发者如是说3 小时前
[中英双语] 如何防止你的 Android 应用被破解
android·安全
giaoho3 小时前
Framework学习:周末小总结以及Binder基础
android
今天的风儿好耀眼3 小时前
关于Google Pixel,或者安卓16,状态栏颜色无法修改的解决方案
android·java·安卓
Digitally4 小时前
使用 6 种方法将文件从 Android 无缝传输到iPad
android·cocoa·ipad