上一节课我们完成了待办清单 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 的完整开发流程,实现了以下功能:
-
数据持久化方案:
- 集成 Dio 实现网络请求,支持任务的增删改查
- 使用 Hive 进行本地数据存储,确保离线可用
- 实现网络与本地数据同步机制,网络失败时自动保存到本地
-
状态管理系统:
- 基于 Riverpod 实现响应式状态管理
- 设计完善的任务状态模型,包含加载状态和错误信息
- 实现数据操作的原子性,确保失败时可回滚或保持本地数据
-
用户体验优化:
- 完善的加载状态和错误处理
- 网络请求失败时提供本地保存选项
- 操作反馈明确,用户始终了解当前状态
- 支持深色模式切换,适应不同使用场景
-
交互设计:
- 列表项滑动删除功能
- 页面跳转动画效果
- 表单操作即时反馈
- 下拉刷新更新数据