Flutter+HarmonyOS跨端实战— 第12篇:历史记录与任务管理
·
实现任务列表和历史记录功能
前言
历史记录是用户查看过往处理任务的重要功能。CleanMark AI 需要记录:
- 处理历史:用户所有的图片/视频去水印任务
- 任务状态:待处理、处理中、已完成、失败
- 结果访问:快速访问历史处理结果
- 任务管理:删除、重新处理
这篇文章我会带你实现完整的历史记录和任务管理功能。

一、功能设计
1.1 任务状态流转
创建任务 → 待处理 (PENDING)
↓
上传文件 → 处理中 (PROCESSING)
↓
AI 处理 → 已完成 (COMPLETED) / 失败 (FAILED)
1.2 数据模型
// lib/features/history/models/task_model.dart
/// 任务模型(统一图片和视频任务)
class TaskModel {
final String id;
final String userId;
final TaskType type; // 任务类型(图片/视频)
final TaskStatus status; // 任务状态
final String inputFile; // 输入文件路径
final String? outputFile; // 输出文件路径
final int creditsUsed; // 消耗积分
final DateTime createdAt;
final DateTime? completedAt;
final String? errorMessage; // 错误信息
const TaskModel({
required this.id,
required this.userId,
required this.type,
required this.status,
required this.inputFile,
this.outputFile,
required this.creditsUsed,
required this.createdAt,
this.completedAt,
this.errorMessage,
});
factory TaskModel.fromJson(Map<String, dynamic> json) {
return TaskModel(
id: json['id'] as String,
userId: json['userId'] as String,
type: TaskType.fromString(json['type'] as String),
status: TaskStatus.fromString(json['status'] as String),
inputFile: json['inputFile'] as String,
outputFile: json['outputFile'] as String?,
creditsUsed: json['creditsUsed'] as int,
createdAt: DateTime.parse(json['createdAt'] as String),
completedAt: json['completedAt'] != null
? DateTime.parse(json['completedAt'] as String)
: null,
errorMessage: json['errorMessage'] as String?,
);
}
/// 是否可以重新处理
bool get canRetry => status == TaskStatus.failed;
/// 是否有结果
bool get hasResult => status == TaskStatus.completed && outputFile != null;
/// 处理耗时
Duration? get processingDuration {
if (completedAt == null) return null;
return completedAt!.difference(createdAt);
}
}
/// 任务类型
enum TaskType {
image, // 图片去水印
video; // 视频去水印
static TaskType fromString(String type) {
switch (type.toUpperCase()) {
case 'IMAGE':
return TaskType.image;
case 'VIDEO':
return TaskType.video;
default:
return TaskType.image;
}
}
String get displayName {
switch (this) {
case TaskType.image:
return '图片去水印';
case TaskType.video:
return '视频去水印';
}
}
IconData get icon {
switch (this) {
case TaskType.image:
return Icons.image;
case TaskType.video:
return Icons.video_library;
}
}
}
/// 任务状态
enum TaskStatus {
pending, // 待处理
processing, // 处理中
completed, // 已完成
failed; // 失败
static TaskStatus fromString(String status) {
switch (status.toUpperCase()) {
case 'PENDING':
return TaskStatus.pending;
case 'PROCESSING':
return TaskStatus.processing;
case 'COMPLETED':
case 'DONE':
return TaskStatus.completed;
case 'FAILED':
return TaskStatus.failed;
default:
return TaskStatus.pending;
}
}
String get displayName {
switch (this) {
case TaskStatus.pending:
return '待处理';
case TaskStatus.processing:
return '处理中';
case TaskStatus.completed:
return '已完成';
case TaskStatus.failed:
return '失败';
}
}
Color get color {
switch (this) {
case TaskStatus.pending:
return AppColors.gray400;
case TaskStatus.processing:
return AppColors.blue;
case TaskStatus.completed:
return AppColors.green;
case TaskStatus.failed:
return AppColors.red;
}
}
}
二、历史记录页面
2.1 页面 UI
// lib/features/history/presentation/pages/history_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/app_colors.dart';
import '../../models/task_model.dart';
import '../providers/history_provider.dart';
import '../widgets/task_list_item.dart';
class HistoryPage extends ConsumerStatefulWidget {
const HistoryPage({super.key});
ConsumerState<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends ConsumerState<HistoryPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// 加载历史记录
Future.microtask(() => ref.read(historyProvider.notifier).loadTasks());
}
void dispose() {
_tabController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final historyState = ref.watch(historyProvider);
return Scaffold(
appBar: AppBar(
title: const Text('历史记录'),
actions: [
IconButton(
icon: const Icon(Icons.delete_sweep),
onPressed: _showClearDialog,
),
],
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '全部'),
Tab(text: '图片'),
Tab(text: '视频'),
],
),
),
body: historyState.when(
data: (tasks) {
if (tasks.isEmpty) {
return _buildEmptyState();
}
return TabBarView(
controller: _tabController,
children: [
_buildTaskList(tasks),
_buildTaskList(tasks.where((t) => t.type == TaskType.image).toList()),
_buildTaskList(tasks.where((t) => t.type == TaskType.video).toList()),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: AppColors.red),
const SizedBox(height: 16),
Text(
'加载失败: $error',
style: const TextStyle(color: AppColors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.read(historyProvider.notifier).loadTasks();
},
child: const Text('重试'),
),
],
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: 64,
color: AppColors.gray500,
),
const SizedBox(height: 16),
Text(
'暂无历史记录',
style: TextStyle(
color: AppColors.gray400,
fontSize: 16,
),
),
],
),
);
}
Widget _buildTaskList(List<TaskModel> tasks) {
if (tasks.isEmpty) {
return _buildEmptyState();
}
return RefreshIndicator(
onRefresh: () async {
await ref.read(historyProvider.notifier).loadTasks();
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: tasks.length,
itemBuilder: (context, index) {
return TaskListItem(
task: tasks[index],
onTap: () => _onTaskTap(tasks[index]),
onDelete: () => _onTaskDelete(tasks[index]),
);
},
),
);
}
void _onTaskTap(TaskModel task) {
if (task.hasResult) {
// 跳转到结果页
Navigator.pushNamed(
context,
task.type == TaskType.image ? '/image-result' : '/video-result',
arguments: task,
);
}
}
void _onTaskDelete(TaskModel task) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.cardDark,
title: const Text(
'确认删除',
style: TextStyle(color: AppColors.white),
),
content: const Text(
'删除后无法恢复,确定要删除这条记录吗?',
style: TextStyle(color: AppColors.gray400),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ref.read(historyProvider.notifier).deleteTask(task.id);
},
child: const Text(
'删除',
style: TextStyle(color: AppColors.red),
),
),
],
),
);
}
void _showClearDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.cardDark,
title: const Text(
'清空历史记录',
style: TextStyle(color: AppColors.white),
),
content: const Text(
'将删除所有历史记录,此操作不可恢复',
style: TextStyle(color: AppColors.gray400),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
ref.read(historyProvider.notifier).clearAllTasks();
},
child: const Text(
'清空',
style: TextStyle(color: AppColors.red),
),
),
],
),
);
}
}
三、任务列表项组件
3.1 TaskListItem Widget
// lib/features/history/presentation/widgets/task_list_item.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:io';
import '../../../../app/app_colors.dart';
import '../../models/task_model.dart';
class TaskListItem extends StatelessWidget {
final TaskModel task;
final VoidCallback onTap;
final VoidCallback onDelete;
const TaskListItem({
required this.task,
required this.onTap,
required this.onDelete,
super.key,
});
Widget build(BuildContext context) {
return Dismissible(
key: Key(task.id),
direction: DismissDirection.endToStart,
background: _buildDismissBackground(),
confirmDismiss: (direction) async {
return await _showDeleteConfirmation(context);
},
onDismissed: (direction) => onDelete(),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// 缩略图
_buildThumbnail(),
const SizedBox(width: 12),
// 任务信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
task.type.icon,
size: 16,
color: AppColors.gray400,
),
const SizedBox(width: 4),
Text(
task.type.displayName,
style: const TextStyle(
color: AppColors.gray400,
fontSize: 12,
),
),
const Spacer(),
_buildStatusBadge(),
],
),
const SizedBox(height: 8),
Text(
_formatDateTime(task.createdAt),
style: const TextStyle(
color: AppColors.white,
fontSize: 14,
),
),
if (task.processingDuration != null) ...[
const SizedBox(height: 4),
Text(
'耗时: ${_formatDuration(task.processingDuration!)}',
style: const TextStyle(
color: AppColors.gray400,
fontSize: 12,
),
),
],
],
),
),
// 操作按钮
IconButton(
icon: const Icon(Icons.more_vert, color: AppColors.gray400),
onPressed: () => _showOptionsMenu(context),
),
],
),
),
),
);
}
Widget _buildThumbnail() {
final isNetwork = task.outputFile?.startsWith('http') ?? false;
final thumbnailUrl = task.outputFile ?? task.inputFile;
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.gray800,
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: task.type == TaskType.image
? (isNetwork
? CachedNetworkImage(
imageUrl: thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
: Image.file(
File(thumbnailUrl),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.error);
},
))
: Stack(
alignment: Alignment.center,
children: [
if (isNetwork)
CachedNetworkImage(
imageUrl: thumbnailUrl,
fit: BoxFit.cover,
)
else
Container(color: AppColors.gray700),
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 20,
),
),
],
),
),
);
}
Widget _buildStatusBadge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: task.status.color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
task.status.displayName,
style: TextStyle(
color: task.status.color,
fontSize: 12,
),
),
);
}
Widget _buildDismissBackground() {
return Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: AppColors.red,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
);
}
Future<bool?> _showDeleteConfirmation(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.cardDark,
title: const Text(
'确认删除',
style: TextStyle(color: AppColors.white),
),
content: const Text(
'确定要删除这条记录吗?',
style: TextStyle(color: AppColors.gray400),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text(
'删除',
style: TextStyle(color: AppColors.red),
),
),
],
),
);
}
void _showOptionsMenu(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: AppColors.cardDark,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (task.hasResult)
ListTile(
leading: const Icon(Icons.visibility, color: AppColors.white),
title: const Text('查看结果', style: TextStyle(color: AppColors.white)),
onTap: () {
Navigator.pop(context);
onTap();
},
),
if (task.canRetry)
ListTile(
leading: const Icon(Icons.refresh, color: AppColors.white),
title: const Text('重新处理', style: TextStyle(color: AppColors.white)),
onTap: () {
Navigator.pop(context);
// TODO: 实现重新处理
},
),
ListTile(
leading: const Icon(Icons.delete, color: AppColors.red),
title: const Text('删除', style: TextStyle(color: AppColors.red)),
onTap: () {
Navigator.pop(context);
onDelete();
},
),
],
),
),
);
}
String _formatDateTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays == 0) {
return '今天 ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else if (difference.inDays == 1) {
return '昨天 ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} else {
return '${dateTime.month}-${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
}
String _formatDuration(Duration duration) {
if (duration.inMinutes > 0) {
return '${duration.inMinutes}分${duration.inSeconds % 60}秒';
}
return '${duration.inSeconds}秒';
}
}
本篇小结(第二部分)
这部分我们完成了:
- ✅ 任务列表项组件(TaskListItem)
- ✅ 缩略图展示(图片/视频)
- ✅ 状态徽章显示
- ✅ 滑动删除功能(Dismissible)
- ✅ 操作菜单(查看结果、重新处理、删除)
下一部分我会继续讲解状态管理和 API 实现。
三、状态管理
3.1 History Provider
// lib/features/history/presentation/providers/history_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/task_model.dart';
import '../../domain/usecases/get_tasks_usecase.dart';
import '../../domain/usecases/delete_task_usecase.dart';
import '../../domain/usecases/clear_tasks_usecase.dart';
/// 历史记录 Provider
final historyProvider = StateNotifierProvider<HistoryNotifier, AsyncValue<List<TaskModel>>>(
(ref) {
final getTasksUseCase = ref.watch(getTasksUseCaseProvider);
final deleteTaskUseCase = ref.watch(deleteTaskUseCaseProvider);
final clearTasksUseCase = ref.watch(clearTasksUseCaseProvider);
return HistoryNotifier(
getTasksUseCase,
deleteTaskUseCase,
clearTasksUseCase,
);
},
);
/// 历史记录状态管理
class HistoryNotifier extends StateNotifier<AsyncValue<List<TaskModel>>> {
final GetTasksUseCase _getTasksUseCase;
final DeleteTaskUseCase _deleteTaskUseCase;
final ClearTasksUseCase _clearTasksUseCase;
HistoryNotifier(
this._getTasksUseCase,
this._deleteTasksUseCase,
this._clearTasksUseCase,
) : super(const AsyncValue.loading());
/// 加载任务列表
Future<void> loadTasks() async {
state = const AsyncValue.loading();
final result = await _getTasksUseCase();
result.fold(
(failure) {
state = AsyncValue.error(failure.message, StackTrace.current);
},
(tasks) {
// 按创建时间倒序排列
tasks.sort((a, b) => b.createdAt.compareTo(a.createdAt));
state = AsyncValue.data(tasks);
},
);
}
/// 删除单个任务
Future<void> deleteTask(String taskId) async {
final result = await _deleteTaskUseCase(taskId);
result.fold(
(failure) {
// 删除失败,保持当前状态
state = AsyncValue.error(failure.message, StackTrace.current);
},
(_) {
// 删除成功,从列表中移除
state.whenData((tasks) {
final updatedTasks = tasks.where((t) => t.id != taskId).toList();
state = AsyncValue.data(updatedTasks);
});
},
);
}
/// 清空所有任务
Future<void> clearAllTasks() async {
final result = await _clearTasksUseCase();
result.fold(
(failure) {
state = AsyncValue.error(failure.message, StackTrace.current);
},
(_) {
state = const AsyncValue.data([]);
},
);
}
/// 刷新任务列表
Future<void> refresh() async {
await loadTasks();
}
}
3.2 UseCase 定义
// lib/features/history/domain/usecases/get_tasks_usecase.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../../models/task_model.dart';
import '../repositories/history_repository.dart';
class GetTasksUseCase {
final HistoryRepository _repository;
GetTasksUseCase(this._repository);
Future<Either<Failure, List<TaskModel>>> call() async {
return await _repository.getTasks();
}
}
/// Provider 定义
final getTasksUseCaseProvider = Provider<GetTasksUseCase>(
(ref) {
final repository = ref.watch(historyRepositoryProvider);
return GetTasksUseCase(repository);
},
);
// lib/features/history/domain/usecases/delete_task_usecase.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../repositories/history_repository.dart';
class DeleteTaskUseCase {
final HistoryRepository _repository;
DeleteTaskUseCase(this._repository);
Future<Either<Failure, void>> call(String taskId) async {
return await _repository.deleteTask(taskId);
}
}
/// Provider 定义
final deleteTaskUseCaseProvider = Provider<DeleteTaskUseCase>(
(ref) {
final repository = ref.watch(historyRepositoryProvider);
return DeleteTaskUseCase(repository);
},
);
// lib/features/history/domain/usecases/clear_tasks_usecase.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../repositories/history_repository.dart';
class ClearTasksUseCase {
final HistoryRepository _repository;
ClearTasksUseCase(this._repository);
Future<Either<Failure, void>> call() async {
return await _repository.clearAllTasks();
}
}
/// Provider 定义
final clearTasksUseCaseProvider = Provider<ClearTasksUseCase>(
(ref) {
final repository = ref.watch(historyRepositoryProvider);
return ClearTasksUseCase(repository);
},
);
本篇小结(第三部分)
这部分我们完成了:
- ✅ HistoryNotifier 状态管理(AsyncValue 处理)
- ✅ 加载、删除、清空任务的方法
- ✅ UseCase 定义(GetTasks、DeleteTask、ClearTasks)
- ✅ Provider 依赖注入配置
下一部分我会继续讲解 Repository 和 API 实现。
四、Repository 和 API 实现
4.1 Repository 接口
// lib/features/history/domain/repositories/history_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../models/task_model.dart';
abstract class HistoryRepository {
/// 获取任务列表
Future<Either<Failure, List<TaskModel>>> getTasks();
/// 删除单个任务
Future<Either<Failure, void>> deleteTask(String taskId);
/// 清空所有任务
Future<Either<Failure, void>> clearAllTasks();
}
4.2 Repository 实现
// lib/features/history/data/repositories/history_repository_impl.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/repositories/history_repository.dart';
import '../../models/task_model.dart';
import '../datasources/history_remote_datasource.dart';
class HistoryRepositoryImpl implements HistoryRepository {
final HistoryRemoteDataSource _remoteDataSource;
HistoryRepositoryImpl(this._remoteDataSource);
Future<Either<Failure, List<TaskModel>>> getTasks() async {
try {
final tasks = await _remoteDataSource.getTasks();
return Right(tasks);
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
Future<Either<Failure, void>> deleteTask(String taskId) async {
try {
await _remoteDataSource.deleteTask(taskId);
return const Right(null);
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
Future<Either<Failure, void>> clearAllTasks() async {
try {
await _remoteDataSource.clearAllTasks();
return const Right(null);
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
}
/// Provider 定义
final historyRepositoryProvider = Provider<HistoryRepository>(
(ref) {
final remoteDataSource = ref.watch(historyRemoteDataSourceProvider);
return HistoryRepositoryImpl(remoteDataSource);
},
);
4.3 Remote DataSource
// lib/features/history/data/datasources/history_remote_datasource.dart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/network/api_client.dart';
import '../../models/task_model.dart';
class HistoryRemoteDataSource {
final ApiClient _apiClient;
HistoryRemoteDataSource(this._apiClient);
/// 获取任务列表
Future<List<TaskModel>> getTasks() async {
try {
final response = await _apiClient.get('/api/tasks');
final List<dynamic> data = response.data as List;
return data.map((json) => TaskModel.fromJson(json)).toList();
} on DioException catch (e) {
throw NetworkException('获取任务列表失败: ${e.message}');
}
}
/// 删除单个任务
Future<void> deleteTask(String taskId) async {
try {
await _apiClient.delete('/api/tasks/$taskId');
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw ServerException('任务不存在');
}
throw NetworkException('删除任务失败: ${e.message}');
}
}
/// 清空所有任务
Future<void> clearAllTasks() async {
try {
await _apiClient.delete('/api/tasks');
} on DioException catch (e) {
throw NetworkException('清空任务失败: ${e.message}');
}
}
}
/// Provider 定义
final historyRemoteDataSourceProvider = Provider<HistoryRemoteDataSource>(
(ref) {
final apiClient = ref.watch(apiClientProvider);
return HistoryRemoteDataSource(apiClient);
},
);
/// 异常类
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}
class ServerException implements Exception {
final String message;
ServerException(this.message);
}
本篇小结(第四部分)
这部分我们完成了:
- ✅ Repository 接口定义
- ✅ Repository 实现(错误处理 + Either 返回)
- ✅ Remote DataSource(API 调用)
- ✅ 异常类定义
下一部分我会总结本篇的关键要点和最佳实践。
五、完整小结
本篇完整小结
这篇文章我们完成了:
- ✅ 任务状态流转设计(pending → processing → completed/failed)
- ✅ 数据模型定义(TaskModel、TaskType、TaskStatus)
- ✅ 历史记录页面(TabBar 分类 + 下拉刷新)
- ✅ 任务列表项组件(缩略图 + 状态徽章 + 滑动删除)
- ✅ 状态管理(HistoryNotifier + AsyncValue)
- ✅ UseCase 实现(GetTasks、DeleteTask、ClearTasks)
- ✅ Repository 和 DataSource(API 调用 + 错误处理)
关键要点:
架构设计
- 采用 Clean Architecture 分层:Presentation → Domain → Data
- 使用 Riverpod 的 StateNotifier 管理列表状态
- AsyncValue 统一处理加载、成功、错误三种状态
- Either 类型封装错误,避免异常穿透到 UI 层
UI 设计
- TabBar 实现分类筛选(全部、图片、视频)
- Dismissible 实现滑动删除,带确认对话框
- 缩略图区分图片和视频(视频显示播放按钮)
- 状态徽章使用不同颜色区分任务状态
- 下拉刷新提供手动更新功能
数据流
UI (HistoryPage)
↓ watch
Provider (historyProvider)
↓ call
UseCase (GetTasksUseCase)
↓ call
Repository (HistoryRepositoryImpl)
↓ call
DataSource (HistoryRemoteDataSource)
↓ HTTP
API Server
状态管理最佳实践
1. AsyncValue 处理三种状态
historyState.when(
data: (tasks) => _buildTaskList(tasks),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => _buildErrorState(error),
);
2. 乐观更新 + 错误回滚
// 删除成功后立即更新 UI
state.whenData((tasks) {
final updatedTasks = tasks.where((t) => t.id != taskId).toList();
state = AsyncValue.data(updatedTasks);
});
3. Provider 依赖注入
final historyProvider = StateNotifierProvider<HistoryNotifier, AsyncValue<List<TaskModel>>>(
(ref) {
final getTasksUseCase = ref.watch(getTasksUseCaseProvider);
final deleteTaskUseCase = ref.watch(deleteTaskUseCaseProvider);
final clearTasksUseCase = ref.watch(clearTasksUseCaseProvider);
return HistoryNotifier(getTasksUseCase, deleteTaskUseCase, clearTasksUseCase);
},
);
错误处理策略
1. 分层错误处理
- DataSource 层:捕获 DioException,转换为业务异常
- Repository 层:捕获业务异常,转换为 Failure
- Presentation 层:展示用户友好的错误信息
2. 错误类型分类
NetworkException → NetworkFailure → "网络连接失败,请检查网络设置"
ServerException → ServerFailure → 显示服务器返回的具体错误
其他异常 → UnknownFailure → "未知错误,请重试"
性能优化
1. 列表优化
- 使用 ListView.builder 而非 ListView(children: […])
- 为列表项提供唯一 key(使用任务 ID)
- 缩略图使用 CachedNetworkImage 缓存
2. 状态优化
- 删除任务时直接更新本地状态,无需重新请求
- 使用 RefreshIndicator 提供手动刷新,减少自动请求
3. 内存优化
- 及时释放 TabController
- 图片使用 fit: BoxFit.cover 避免内存溢出
用户体验优化
1. 交互反馈
- 滑动删除带确认对话框,防止误操作
- 删除后显示 SnackBar 提示
- 空状态显示友好提示
2. 时间格式化
今天 14:30
昨天 09:15
05-20 18:45
3. 操作菜单
- 查看结果(已完成任务)
- 重新处理(失败任务)
- 删除(所有任务)
思考题
- 如何实现任务列表的分页加载?(提示:offset + limit)
- 如果删除任务时网络失败,如何回滚 UI 状态?
- 如何实现任务列表的本地缓存,离线也能查看?(提示:Isar 或 Drift)
下一篇预告:第13篇 - 网络请求封装与错误处理
更多推荐


所有评论(0)