实现任务列表和历史记录功能

前言

历史记录是用户查看过往处理任务的重要功能。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}秒';
  }
}

本篇小结(第二部分)

这部分我们完成了:

  1. ✅ 任务列表项组件(TaskListItem)
  2. ✅ 缩略图展示(图片/视频)
  3. ✅ 状态徽章显示
  4. ✅ 滑动删除功能(Dismissible)
  5. ✅ 操作菜单(查看结果、重新处理、删除)

下一部分我会继续讲解状态管理和 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);
  },
);

本篇小结(第三部分)

这部分我们完成了:

  1. ✅ HistoryNotifier 状态管理(AsyncValue 处理)
  2. ✅ 加载、删除、清空任务的方法
  3. ✅ UseCase 定义(GetTasks、DeleteTask、ClearTasks)
  4. ✅ 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);
}

本篇小结(第四部分)

这部分我们完成了:

  1. ✅ Repository 接口定义
  2. ✅ Repository 实现(错误处理 + Either 返回)
  3. ✅ Remote DataSource(API 调用)
  4. ✅ 异常类定义

下一部分我会总结本篇的关键要点和最佳实践。


五、完整小结

本篇完整小结

这篇文章我们完成了:

  1. ✅ 任务状态流转设计(pending → processing → completed/failed)
  2. ✅ 数据模型定义(TaskModel、TaskType、TaskStatus)
  3. ✅ 历史记录页面(TabBar 分类 + 下拉刷新)
  4. ✅ 任务列表项组件(缩略图 + 状态徽章 + 滑动删除)
  5. ✅ 状态管理(HistoryNotifier + AsyncValue)
  6. ✅ UseCase 实现(GetTasks、DeleteTask、ClearTasks)
  7. ✅ 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. 错误类型分类

NetworkExceptionNetworkFailure"网络连接失败,请检查网络设置"
ServerExceptionServerFailure   → 显示服务器返回的具体错误
其他异常          → 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. 操作菜单

  • 查看结果(已完成任务)
  • 重新处理(失败任务)
  • 删除(所有任务)

思考题

  1. 如何实现任务列表的分页加载?(提示:offset + limit)
  2. 如果删除任务时网络失败,如何回滚 UI 状态?
  3. 如何实现任务列表的本地缓存,离线也能查看?(提示:Isar 或 Drift)

下一篇预告:第13篇 - 网络请求封装与错误处理


Logo

一站式 AI 云服务平台

更多推荐