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. 交互设计

    • 列表项滑动删除功能
    • 页面跳转动画效果
    • 表单操作即时反馈
    • 下拉刷新更新数据
相关推荐
修炼者3 分钟前
Toast的显示流程
android
simplepeng3 小时前
Room 3.0 KMP Alpha-01
android·kotlin·android jetpack
Lei活在当下3 小时前
Windows 下 Codex 高效工作流最佳实践
android·openai·ai编程
fatiaozhang95273 小时前
基于slimBOXtv 9.19.0 v4(通刷晶晨S905L3A/L3AB芯片)ATV-安卓9-完美版线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·晶晨s905l3ab·晶晨s905l3a
私房菜4 小时前
Selinux 及在Android 的使用详解
android·selinux·sepolicy
一只特立独行的Yang5 小时前
Android中的系统级共享库
android
两个人的幸福online5 小时前
php开发者 需要 协程吗
android·开发语言·php
始持5 小时前
第二讲 Flutter 文字、图片与图标(基础视觉元素)
flutter
修炼者6 小时前
WindowManager(WMS)构建全局悬浮窗
android
xiaoshiquan12067 小时前
Android Studio里,SDK Manager显示不全问题
android·ide·android studio