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

上一节课我们完成了待办清单 App 的基础页面搭建,这节课我们将在此基础上,实现数据持久化、状态管理、交互优化和深色模式适配,让 App 具备更完善的功能和更好的用户体验。

一、集成网络请求与本地存储

1. 网络请求实现(以 Dio 为例)

首先在pubspec.yaml中添加依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.9.0
  flutter_riverpod: ^2.6.1
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  intl: ^0.20.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.6.0

创建网络请求工具类utils/network_utils.dart

dart 复制代码
import 'package:dio/dio.dart';
import '../models/task_model.dart';

class NetworkUtils {
  static final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com/tasks',
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 3),
    ),
  );

  // 获取所有任务
  static Future<List<Task>> fetchTasks() async {
    try {
      final response = await _dio.get('/');
      List<dynamic> data = response.data;
      return data.map((json) => Task.fromJson(json)).toList();
    } catch (e) {
      throw Exception('Failed to fetch tasks: $e');
    }
  }

  // 添加新任务
  static Future<Task> addTask(Task task) async {
    try {
      final response = await _dio.post('/', data: task.toJson());
      return Task.fromJson(response.data);
    } catch (e) {
      throw Exception('Failed to add task: $e');
    }
  }

  // 更新任务
  static Future<Task> updateTask(Task task) async {
    try {
      final response = await _dio.put('/${task.id}', data: task.toJson());
      return Task.fromJson(response.data);
    } catch (e) {
      throw Exception('Failed to update task: $e');
    }
  }

  // 删除任务
  static Future<void> deleteTask(String id) async {
    try {
      await _dio.delete('/$id');
    } catch (e) {
      throw Exception('Failed to delete task: $e');
    }
  }

  // 统一错误提示方法
  static String getErrorMsg(dynamic e) {
    if (e is DioException) {
      if (e.type == DioExceptionType.connectionTimeout) {
        return '网络连接超时,请检查网络';
      } else if (e.type == DioExceptionType.receiveTimeout) {
        return '数据接收超时';
      } else if (e.type == DioExceptionType.badResponse) {
        return '服务器错误:${e.response?.statusCode}';
      } else if (e.type == DioExceptionType.connectionError) {
        return '无法连接到服务器,请检查网络';
      }
    }
    return '操作失败,请稍后重试';
  }
}

修改 Task 模型,使用手动 JSON 序列化(models/task_model.dart):

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();

  // 手动实现JSON序列化
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'createTime': createTime.millisecondsSinceEpoch,
      'deadline': deadline?.millisecondsSinceEpoch,
      'isCompleted': isCompleted,
    };
  }

  // 手动实现从JSON反序列化
  factory Task.fromJson(Map<String, dynamic> json) {
    return Task(
      id: json['id'],
      title: json['title'],
      content: json['content'] ?? '',
      createTime: DateTime.fromMillisecondsSinceEpoch(json['createTime']),
      deadline: json['deadline'] != null
          ? DateTime.fromMillisecondsSinceEpoch(json['deadline'])
          : null,
      isCompleted: json['isCompleted'] ?? false,
    );
  }
}

2. 本地存储实现(Hive)

创建 Hive 适配器(models/task_adapter.dart):

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

class TaskAdapter extends TypeAdapter<Task> {
  @override
  final typeId = 0;

  @override
  Task read(BinaryReader reader) {
    final id = reader.readString();
    final title = reader.readString();
    final content = reader.readString();
    final createTime = DateTime.fromMillisecondsSinceEpoch(reader.readInt());
    final hasDeadline = reader.readBool();
    DateTime? deadline;
    if (hasDeadline) {
      deadline = DateTime.fromMillisecondsSinceEpoch(reader.readInt());
    }
    final isCompleted = reader.readBool();

    return Task(
      id: id,
      title: title,
      content: content,
      createTime: createTime,
      deadline: deadline,
      isCompleted: isCompleted,
    );
  }

  @override
  void write(BinaryWriter writer, Task obj) {
    writer.writeString(obj.id);
    writer.writeString(obj.title);
    writer.writeString(obj.content);
    writer.writeInt(obj.createTime.millisecondsSinceEpoch);
    writer.writeBool(obj.deadline != null);
    if (obj.deadline != null) {
      writer.writeInt(obj.deadline!.millisecondsSinceEpoch);
    }
    writer.writeBool(obj.isCompleted);
  }
}

初始化 Hive(main.dart):

dart 复制代码
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'models/task_model.dart';
import 'models/task_adapter.dart';
import 'pages/home_page.dart';
import 'utils/router.dart';
import 'providers/theme_provider.dart';

void main() async {
  // 初始化Hive
  await Hive.initFlutter();
  // 注册适配器
  Hive.registerAdapter(TaskAdapter());
  // 打开任务盒子
  await Hive.openBox<Task>('tasks');
  // 打开设置盒子
  await Hive.openBox('settings');
  
  runApp(ProviderScope(child: const MyApp()));
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);

    return MaterialApp(
      title: '待办清单',
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        scaffoldBackgroundColor: Colors.white,
        cardColor: Colors.white,
        textTheme: const TextTheme(
          bodyLarge: TextStyle(color: Colors.black87),
          bodyMedium: TextStyle(color: Colors.black54),
        ),
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        scaffoldBackgroundColor: Colors.grey[900],
        cardColor: Colors.grey[800],
        textTheme: const TextTheme(
          bodyLarge: TextStyle(color: Colors.white70),
          bodyMedium: TextStyle(color: Colors.white54),
        ),
      ),
      themeMode: themeMode,
      initialRoute: Router.home,
      routes: Router.routes,
      onGenerateRoute: Router.generateRoute,
      debugShowCheckedModeBanner: false,
    );
  }
}

创建本地存储工具类(utils/storage_utils.dart):

dart 复制代码
import 'package:hive/hive.dart';
import '../models/task_model.dart';

class StorageUtils {
  static Box<Task> get _taskBox => Hive.box<Task>('tasks');

  // 保存任务列表
  static Future<void> saveTasks(List<Task> tasks) async {
    await _taskBox.clear();
    await _taskBox.addAll(tasks);
  }

  // 获取所有任务
  static List<Task> getTasks() => _taskBox.values.toList();

  // 添加单个任务
  static Future<void> addTask(Task task) async {
    await _taskBox.put(task.id, task);
  }

  // 更新任务
  static Future<void> updateTask(Task task) async {
    await _taskBox.put(task.id, task);
  }

  // 删除任务
  static Future<void> deleteTask(String id) async {
    await _taskBox.delete(id);
  }
}

二、状态管理方案落地(Riverpod)

创建任务状态管理类(providers/task_provider.dart):

dart 复制代码
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/task_model.dart';
import '../utils/network_utils.dart';
import '../utils/storage_utils.dart';

// 定义任务状态数据类,包含任务列表和加载状态
class TaskState {
  final List<Task> tasks;
  final bool isLoading;
  final String? errorMessage;

  TaskState({required this.tasks, required this.isLoading, this.errorMessage});

  // 复制方法,便于状态更新
  TaskState copyWith({
    List<Task>? tasks,
    bool? isLoading,
    String? errorMessage,
  }) {
    return TaskState(
      tasks: tasks ?? this.tasks,
      isLoading: isLoading ?? this.isLoading,
      errorMessage: errorMessage,
    );
  }
}

// 任务状态提供者
final taskProvider = StateNotifierProvider<TaskNotifier, TaskState>((ref) {
  return TaskNotifier();
});

class TaskNotifier extends StateNotifier<TaskState> {
  TaskNotifier() : super(TaskState(tasks: [], isLoading: true)) {
    loadTasks(); // 初始化时加载任务
  }

  // 从本地存储加载任务
  Future<void> loadTasks() async {
    state = state.copyWith(isLoading: true, errorMessage: null);

    try {
      // 先加载本地数据
      final localTasks = StorageUtils.getTasks();
      state = state.copyWith(tasks: localTasks);

      // 再尝试从网络获取最新数据
      final networkTasks = await NetworkUtils.fetchTasks();
      state = state.copyWith(tasks: networkTasks);
      await StorageUtils.saveTasks(networkTasks);
    } catch (e) {
      // 网络请求失败时保持本地数据(如果有)
      final errorMsg = NetworkUtils.getErrorMsg(e);
      state = state.copyWith(errorMessage: errorMsg, isLoading: false);
    } finally {
      if (mounted) {
        state = state.copyWith(isLoading: false);
      }
    }
  }

  // 添加任务 - 网络失败时仍保存到本地
  Future<void> addTask(Task task, {bool skipNetwork = false}) async {
    final currentTasks = List<Task>.from(state.tasks);

    try {
      // 先更新UI
      state = state.copyWith(tasks: [...state.tasks, task], errorMessage: null);

      // 保存到本地(无论网络是否成功都执行)
      await StorageUtils.addTask(task);

      // 如果不需要网络请求或网络请求成功
      if (skipNetwork) {
        return;
      }

      // 尝试同步到网络
      final newTask = await NetworkUtils.addTask(task);
      // 更新为网络返回的任务
      state = state.copyWith(
        tasks: state.tasks.map((t) => t.id == task.id ? newTask : t).toList(),
      );
    } catch (e) {
      // 网络失败时保持本地保存的状态,不回滚
      state = state.copyWith(errorMessage: NetworkUtils.getErrorMsg(e));
      rethrow;
    }
  }

  // 更新任务 - 网络失败时仍保存到本地
  Future<void> updateTask(Task task, {bool skipNetwork = false}) async {
    final currentTask = state.tasks.firstWhere(
      (t) => t.id == task.id,
      orElse: () => task,
    );

    try {
      // 先更新UI
      state = state.copyWith(
        tasks: state.tasks.map((t) => t.id == task.id ? task : t).toList(),
        errorMessage: null,
      );

      // 保存到本地(无论网络是否成功都执行)
      await StorageUtils.updateTask(task);

      // 如果不需要网络请求
      if (skipNetwork) {
        return;
      }

      // 尝试同步到网络
      final updatedTask = await NetworkUtils.updateTask(task);
    } catch (e) {
      // 网络失败时保持本地更新的状态
      state = state.copyWith(errorMessage: NetworkUtils.getErrorMsg(e));
      rethrow;
    }
  }

  // 删除任务
  Future<void> deleteTask(String id) async {
    // 保存要删除的任务用于失败时回滚
    final deletedTask = state.tasks.firstWhere(
      (t) => t.id == id,
      orElse: () => Task(id: id, title: ''),
    );

    try {
      // 先更新UI
      state = state.copyWith(
        tasks: state.tasks.where((t) => t.id != id).toList(),
        errorMessage: null,
      );

      // 同步到网络
      await NetworkUtils.deleteTask(id);
      // 同步到本地
      await StorageUtils.deleteTask(id);
    } catch (e) {
      // 失败时回滚
      state = state.copyWith(
        tasks: [...state.tasks, deletedTask],
        errorMessage: NetworkUtils.getErrorMsg(e),
      );
      rethrow;
    }
  }
}

创建主题状态管理(providers/theme_provider.dart):

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

final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
  (ref) => ThemeModeNotifier(),
);

class ThemeModeNotifier extends StateNotifier<ThemeMode> {
  static const _themeBox = 'settings';
  static const _themeKey = 'theme_mode';

  ThemeModeNotifier() : super(ThemeMode.system) {
    _loadThemeMode();
  }

  Future<void> _loadThemeMode() async {
    final box = await Hive.openBox(_themeBox);
    final mode = box.get(_themeKey);
    if (mode == 'light') {
      state = ThemeMode.light;
    } else if (mode == 'dark') {
      state = ThemeMode.dark;
    } else {
      state = ThemeMode.system;
    }
  }

  void setThemeMode(ThemeMode mode) {
    state = mode;
    final box = Hive.box(_themeBox);
    if (mode == ThemeMode.light) {
      box.put(_themeKey, 'light');
    } else if (mode == ThemeMode.dark) {
      box.put(_themeKey, 'dark');
    } else {
      box.put(_themeKey, 'system');
    }
  }
}

三、完善核心页面实现

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

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

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final taskState = ref.watch(taskProvider);
    final taskNotifier = ref.read(taskProvider.notifier);

    // 首次加载任务数据
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (taskState.tasks.isEmpty && taskState.isLoading) {
        taskNotifier.loadTasks();
      }
    });

    // 显示错误信息
    if (taskState.errorMessage != null && taskState.tasks.isEmpty) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (context.mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(taskState.errorMessage!),
              backgroundColor: Colors.red,
              action: SnackBarAction(
                label: '重试',
                onPressed: () => taskNotifier.loadTasks(),
              ),
            ),
          );
        }
      });
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('待办清单'),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const SettingPage()),
            ),
          ),
        ],
      ),
      body: _buildBody(context, taskState, taskNotifier),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () => _navigateToDetail(context, null, taskNotifier),
      ),
    );
  }

  Widget _buildBody(
    BuildContext context,
    TaskState taskState,
    TaskNotifier taskNotifier,
  ) {
    // 加载中状态
    if (taskState.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    // 空数据状态
    if (taskState.tasks.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('暂无任务,添加一个吧!'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => taskNotifier.loadTasks(),
              child: const Text('重新加载'),
            ),
          ],
        ),
      );
    }

    // 正常显示任务列表
    return RefreshIndicator(
      onRefresh: () async {
        await taskNotifier.loadTasks();
      },
      child: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: taskState.tasks.length,
        itemBuilder: (context, index) {
          final task = taskState.tasks[index];
          return TaskItem(
            task: task,
            onTap: () => _navigateToDetail(context, task, taskNotifier),
            onStatusChanged: (id) {
              final updatedTask = Task(
                id: task.id,
                title: task.title,
                content: task.content,
                createTime: task.createTime,
                deadline: task.deadline,
                isCompleted: !task.isCompleted,
              );
              taskNotifier.updateTask(updatedTask);
            },
            onDelete: (id) => taskNotifier.deleteTask(id),
          );
        },
      ),
    );
  }

  void _navigateToDetail(
    BuildContext context,
    Task? task,
    TaskNotifier notifier,
  ) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => DetailPage(
          task: task,
          onSave: (newTask) async {
            try {
              if (task == null) {
                // 首次尝试带网络请求的保存
                await notifier.addTask(newTask);
                if (context.mounted) {
                  ScaffoldMessenger.of(
                    context,
                  ).showSnackBar(const SnackBar(content: Text('任务添加成功')));
                  Navigator.pop(context);
                }
              } else {
                // 首次尝试带网络请求的更新
                await notifier.updateTask(newTask);
                if (context.mounted) {
                  ScaffoldMessenger.of(
                    context,
                  ).showSnackBar(const SnackBar(content: Text('任务更新成功')));
                  Navigator.pop(context);
                }
              }
            } catch (e) {
              if (context.mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('${NetworkUtils.getErrorMsg(e)},将保存到本地'),
                    action: SnackBarAction(
                      label: '确认',
                      onPressed: () =>
                          _saveAgain(context, newTask, task, notifier),
                    ),
                  ),
                );
              }
            }
          },
        ),
      ),
    );
  }

  // 重新尝试保存任务 - 仅保存到本地,跳过网络请求
  void _saveAgain(
    BuildContext context,
    Task newTask,
    Task? originalTask,
    TaskNotifier notifier,
  ) {
    if (originalTask == null) {
      // 跳过网络请求,仅本地保存
      notifier.addTask(newTask, skipNetwork: true);
    } else {
      // 跳过网络请求,仅本地保存
      notifier.updateTask(newTask, skipNetwork: true);
    }

    if (context.mounted) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('任务已保存到本地')));
      Navigator.pop(context);
    }
  }
}

2. 任务列表项组件(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 Dismissible(
      key: Key(task.id),
      direction: DismissDirection.endToStart,
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      confirmDismiss: (direction) async {
        return await showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('确认删除'),
            content: const Text('确定要删除这个任务吗?'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context, false),
                child: const Text('取消'),
              ),
              TextButton(
                onPressed: () => Navigator.pop(context, true),
                child: const Text('删除', style: TextStyle(color: Colors.red)),
              ),
            ],
          ),
        );
      },
      onDismissed: (direction) => onDelete(task.id),
      child: 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),
        ),
      ),
    );
  }
}

3. 详情页实现(pages/detail_page.dart

dart 复制代码
import 'package:flutter/material.dart' hide DateUtils;
import '../models/task_model.dart';
import '../utils/date_utils.dart';
import '../utils/network_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;
  bool _isSaving = 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() async {
    if (_titleController.text.isEmpty) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('请输入任务标题')));
      return;
    }

    setState(() => _isSaving = true);
    try {
      // 创建任务对象(完整参数)
      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,
      );

      // 调用回调保存任务
      await widget.onSave(task);

      // 保存成功后返回首页
      if (mounted) {
        Navigator.pop(context);
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text(NetworkUtils.getErrorMsg(e))));
      }
    } finally {
      if (mounted) {
        setState(() => _isSaving = false);
      }
    }
  }

  // 选择截止日期
  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: () async {
                final confirm = await showDialog<bool>(
                  context: context,
                  builder: (context) => AlertDialog(
                    title: const Text('确认删除'),
                    content: const Text('确定要删除这个任务吗?'),
                    actions: [
                      TextButton(
                        onPressed: () => Navigator.pop(context, false),
                        child: const Text('取消'),
                      ),
                      TextButton(
                        onPressed: () => Navigator.pop(context, true),
                        child: const Text(
                          '删除',
                          style: TextStyle(color: Colors.red),
                        ),
                      ),
                    ],
                  ),
                );
                if (confirm == true && mounted) {
                  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: _isSaving ? null : _saveTask,
              child: _isSaving
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                      ),
                    )
                  : const Padding(
                      padding: EdgeInsets.all(12),
                      child: Text('保存任务'),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

4. 设置页实现(pages/setting_page.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import '../providers/theme_provider.dart';
import '../models/task_model.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);
    final themeNotifier = ref.read(themeModeProvider.notifier);

    return Scaffold(
      appBar: AppBar(title: const Text('设置')),
      body: ListView(
        children: [
          // 通知设置
          SwitchListTile(
            title: const Text('开启通知提醒'),
            value: true, // 模拟开启状态
            onChanged: (value) {
              // 这里可以实现通知开关逻辑
            },
          ),
          // 深色模式设置
          ListTile(
            title: const Text('深色模式'),
            trailing: DropdownButton<ThemeMode>(
              value: themeMode,
              items: const [
                DropdownMenuItem(value: ThemeMode.system, child: Text('跟随系统')),
                DropdownMenuItem(value: ThemeMode.light, child: Text('浅色模式')),
                DropdownMenuItem(value: ThemeMode.dark, child: Text('深色模式')),
              ],
              onChanged: (value) {
                if (value != null) {
                  themeNotifier.setThemeMode(value);
                }
              },
            ),
          ),
          // 关于我们
          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: () async {
              // 清理任务缓存
              await Hive.box<Task>('tasks').clear();
              // 显示提示 - 使用context判断而非mounted
              if (context.mounted) {
                ScaffoldMessenger.of(
                  context,
                ).showSnackBar(const SnackBar(content: Text('缓存已清理')));
              }
            },
          ),
        ],
      ),
    );
  }
}

5. 路由管理工具(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 PageRouteBuilder(
          transitionDuration: const Duration(milliseconds: 300),
          pageBuilder: (context, animation, secondaryAnimation) =>
              DetailPage(task: args?['task'], onSave: args?['onSave']),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            const begin = Offset(1.0, 0.0);
            const end = Offset.zero;
            const curve = Curves.ease;
            final tween = Tween(
              begin: begin,
              end: end,
            ).chain(CurveTween(curve: curve));
            return SlideTransition(
              position: animation.drive(tween),
              child: child,
            );
          },
        );
      default:
        return MaterialPageRoute(
          builder: (context) =>
              const Scaffold(body: Center(child: Text('页面不存在'))),
        );
    }
  }
}

6. 日期工具类(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);
    }
  }
}

四、总结与扩展

通过本节课的学习,我们完成了待办清单 App 的完整开发流程,实现了以下功能:

  1. 数据持久化方案

    • 集成 Dio 实现网络请求,支持任务的增删改查
    • 使用 Hive 进行本地数据存储,确保离线可用
    • 实现网络与本地数据同步机制,网络失败时自动保存到本地
  2. 状态管理系统

    • 基于 Riverpod 实现响应式状态管理
    • 设计完善的任务状态模型,包含加载状态和错误信息
    • 实现数据操作的原子性,确保失败时可回滚或保持本地数据
  3. 用户体验优化

    • 完善的加载状态和错误处理
    • 网络请求失败时提供本地保存选项
    • 操作反馈明确,用户始终了解当前状态
    • 支持深色模式切换,适应不同使用场景
  4. 交互设计

    • 列表项滑动删除功能
    • 页面跳转动画效果
    • 表单操作即时反馈
    • 下拉刷新更新数据
相关推荐
用户20187928316740 分钟前
🌟 一场失败的加密舞会:SSL握手失败的奇幻冒险
android
tangweiguo030519872 小时前
面向对象编程三剑客:Dart、Java 和 Kotlin 的核心区别
android·flutter·kotlin
幼稚园的山代王2 小时前
Kotlin数据类型
android·开发语言·kotlin
xixixin_3 小时前
【H5】禁止IOS、安卓端长按的一些默认操作
android·css·ios·h5
zhangphil3 小时前
Android实现Glide/Coil样式图/视频加载框架,Kotlin
android·kotlin
用户2018792831673 小时前
“记忆邮局” (LiveData)
android
小林up3 小时前
HiSmartPerf使用WIFI方式连接Android机显示当前设备0.0.0.0无法ping通!设备和电脑连接同一网络,将设备保持亮屏重新尝试
android·网络·电脑
Andy_GF4 小时前
纯血鸿蒙 HarmonyOS Next 调试证书过期解决流程
前端·ios·harmonyos
用户2018792831675 小时前
Dialog不消失之谜——Android窗口系统的"平行宇宙"
android