Flutter+HarmonyOS跨端实战—第09篇:图片去水印功能实现
·
实现 CleanMark AI 的核心功能
前言
图片去水印是 CleanMark AI 的核心功能。用户上传带水印的图片,AI 自动识别并去除水印,返回处理后的图片。
这篇文章我会带你实现完整的图片去水印流程,包括:
- 图片选择和上传
- 调用 AI 服务处理
- 显示处理结果
- 下载和分享
一、功能流程设计
1.1 业务流程图
用户点击"去水印"
↓
选择图片来源(相册/相机)
↓
图片选择器
↓
预览图片
↓
确认上传
↓
检查积分(≥1)
↓
上传到服务器
↓
调用 AI 服务处理
↓
显示处理结果
↓
下载/分享

1.2 数据流
// lib/features/inpaint/models/inpaint_task.dart
/// 去水印任务模型
class InpaintTask {
final String id;
final String inputFile; // 输入文件路径
final String? outputFile; // 输出文件路径
final TaskStatus status; // 任务状态
final DateTime createdAt;
final DateTime? completedAt;
const InpaintTask({
required this.id,
required this.inputFile,
this.outputFile,
required this.status,
required this.createdAt,
this.completedAt,
});
factory InpaintTask.fromJson(Map<String, dynamic> json) {
return InpaintTask(
id: json['id'] as String,
inputFile: json['inputFile'] as String,
outputFile: json['outputFile'] as String?,
status: TaskStatus.fromString(json['status'] as String),
createdAt: DateTime.parse(json['createdAt'] as String),
completedAt: json['completedAt'] != null
? DateTime.parse(json['completedAt'] as String)
: null,
);
}
}
/// 任务状态
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 'DONE':
case 'COMPLETED':
return TaskStatus.completed;
case 'FAILED':
return TaskStatus.failed;
default:
return TaskStatus.pending;
}
}
}
二、图片上传页面
2.1 页面 UI
// lib/features/inpaint/presentation/pages/image_upload_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import '../../../../app/app_colors.dart';
import '../../../../shared/widgets/gradient_button.dart';
import '../providers/inpaint_provider.dart';
class ImageUploadPage extends ConsumerStatefulWidget {
const ImageUploadPage({super.key});
ConsumerState<ImageUploadPage> createState() => _ImageUploadPageState();
}
class _ImageUploadPageState extends ConsumerState<ImageUploadPage> {
File? _selectedImage;
final ImagePicker _picker = ImagePicker();
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? image = await _picker.pickImage(
source: source,
maxWidth: 2048,
maxHeight: 2048,
imageQuality: 85,
);
if (image != null) {
setState(() {
_selectedImage = File(image.path);
});
}
} catch (e) {
_showError('选择图片失败: $e');
}
}
Future<void> _uploadAndProcess() async {
if (_selectedImage == null) {
_showError('请先选择图片');
return;
}
// 调用 Provider 上传处理
final result = await ref.read(inpaintProvider.notifier).removeWatermark(
_selectedImage!,
);
if (!mounted) return;
result.fold(
(failure) => _showError(failure.message),
(task) {
// 跳转到结果页
Navigator.pushNamed(
context,
'/result',
arguments: task,
);
},
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.red,
),
);
}
Widget build(BuildContext context) {
final inpaintState = ref.watch(inpaintProvider);
return Scaffold(
appBar: AppBar(
title: const Text('图片去水印'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 图片预览区域
_buildImagePreview(),
const SizedBox(height: 24),
// 选择图片按钮
if (_selectedImage == null) ...[
_buildPickButton(
icon: Icons.photo_library,
label: '从相册选择',
onTap: () => _pickImage(ImageSource.gallery),
),
const SizedBox(height: 12),
_buildPickButton(
icon: Icons.camera_alt,
label: '拍照',
onTap: () => _pickImage(ImageSource.camera),
),
],
// 上传按钮
if (_selectedImage != null) ...[
const SizedBox(height: 24),
inpaintState.isLoading
? const Center(child: CircularProgressIndicator())
: GradientButton(
text: '开始去水印',
onPressed: _uploadAndProcess,
),
const SizedBox(height: 12),
TextButton(
onPressed: () {
setState(() {
_selectedImage = null;
});
},
child: const Text('重新选择'),
),
],
const SizedBox(height: 24),
// 提示信息
_buildTips(),
],
),
),
);
}
Widget _buildImagePreview() {
return Container(
height: 300,
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.gray700,
width: 2,
style: BorderStyle.solid,
),
),
child: _selectedImage != null
? ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Image.file(
_selectedImage!,
fit: BoxFit.contain,
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.image_outlined,
size: 64,
color: AppColors.gray500,
),
const SizedBox(height: 16),
Text(
'请选择要去水印的图片',
style: TextStyle(
color: AppColors.gray400,
fontSize: 16,
),
),
],
),
),
);
}
Widget _buildPickButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.gray700),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: AppColors.purple),
),
const SizedBox(width: 16),
Text(
label,
style: const TextStyle(
color: AppColors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildTips() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.blue.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: AppColors.blue,
size: 20,
),
const SizedBox(width: 8),
const Text(
'使用提示',
style: TextStyle(
color: AppColors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
'• 支持 JPG、PNG 格式\n'
'• 图片大小不超过 10MB\n'
'• 每次处理消耗 1 积分\n'
'• 处理时间约 5-10 秒',
style: TextStyle(
color: AppColors.gray400,
fontSize: 14,
height: 1.5,
),
),
],
),
);
}
}
本篇小结(第一部分)
这部分我们完成了:
- ✅ 功能流程设计
- ✅ 数据模型定义(InpaintTask)
- ✅ 图片上传页面 UI
下一部分我会继续讲解状态管理和 API 调用。
三、状态管理
3.1 Inpaint Provider
// lib/features/inpaint/presentation/providers/inpaint_provider.dart
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/usecases/remove_watermark_usecase.dart';
import '../../models/inpaint_task.dart';
/// Inpaint 状态
class InpaintState {
final bool isLoading;
final InpaintTask? currentTask;
final String? errorMessage;
const InpaintState({
this.isLoading = false,
this.currentTask,
this.errorMessage,
});
InpaintState copyWith({
bool? isLoading,
InpaintTask? currentTask,
String? errorMessage,
}) {
return InpaintState(
isLoading: isLoading ?? this.isLoading,
currentTask: currentTask ?? this.currentTask,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
/// Inpaint Provider
class InpaintNotifier extends StateNotifier<InpaintState> {
final RemoveWatermarkUseCase _removeWatermarkUseCase;
InpaintNotifier(this._removeWatermarkUseCase)
: super(const InpaintState());
/// 去除水印
Future<Either<Failure, InpaintTask>> removeWatermark(File imageFile) async {
state = state.copyWith(isLoading: true, errorMessage: null);
final result = await _removeWatermarkUseCase(imageFile);
return result.fold(
(failure) {
state = state.copyWith(
isLoading: false,
errorMessage: failure.message,
);
return Left(failure);
},
(task) {
state = state.copyWith(
isLoading: false,
currentTask: task,
);
return Right(task);
},
);
}
/// 清除当前任务
void clearCurrentTask() {
state = state.copyWith(currentTask: null);
}
}
/// Provider 定义
final inpaintProvider = StateNotifierProvider<InpaintNotifier, InpaintState>(
(ref) {
final removeWatermarkUseCase = ref.watch(removeWatermarkUseCaseProvider);
return InpaintNotifier(removeWatermarkUseCase);
},
);
3.2 UseCase 实现
// lib/features/inpaint/domain/usecases/remove_watermark_usecase.dart
import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../../models/inpaint_task.dart';
import '../repositories/inpaint_repository.dart';
class RemoveWatermarkUseCase {
final InpaintRepository _repository;
RemoveWatermarkUseCase(this._repository);
Future<Either<Failure, InpaintTask>> call(File imageFile) async {
// 验证文件
if (!imageFile.existsSync()) {
return const Left(ValidationFailure('文件不存在'));
}
// 检查文件大小(最大 10MB)
final fileSize = await imageFile.length();
if (fileSize > 10 * 1024 * 1024) {
return const Left(ValidationFailure('文件大小不能超过 10MB'));
}
// 检查文件格式
final extension = imageFile.path.split('.').last.toLowerCase();
if (!['jpg', 'jpeg', 'png'].contains(extension)) {
return const Left(ValidationFailure('仅支持 JPG、PNG 格式'));
}
// 调用 Repository
return await _repository.removeWatermark(imageFile);
}
}
/// Provider 定义
final removeWatermarkUseCaseProvider = Provider<RemoveWatermarkUseCase>(
(ref) {
final repository = ref.watch(inpaintRepositoryProvider);
return RemoveWatermarkUseCase(repository);
},
);
四、API 服务层
4.1 Repository 接口
// lib/features/inpaint/domain/repositories/inpaint_repository.dart
import 'dart:io';
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../models/inpaint_task.dart';
abstract class InpaintRepository {
/// 去除水印
Future<Either<Failure, InpaintTask>> removeWatermark(File imageFile);
/// 获取任务详情
Future<Either<Failure, InpaintTask>> getTask(String taskId);
/// 获取任务列表
Future<Either<Failure, List<InpaintTask>>> getTasks();
}
4.2 Repository 实现
// lib/features/inpaint/data/repositories/inpaint_repository_impl.dart
import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/repositories/inpaint_repository.dart';
import '../../models/inpaint_task.dart';
import '../datasources/inpaint_remote_datasource.dart';
class InpaintRepositoryImpl implements InpaintRepository {
final InpaintRemoteDataSource _remoteDataSource;
InpaintRepositoryImpl(this._remoteDataSource);
Future<Either<Failure, InpaintTask>> removeWatermark(File imageFile) async {
try {
final task = await _remoteDataSource.removeWatermark(imageFile);
return Right(task);
} 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, InpaintTask>> getTask(String taskId) async {
try {
final task = await _remoteDataSource.getTask(taskId);
return Right(task);
} 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, List<InpaintTask>>> 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()));
}
}
}
/// Provider 定义
final inpaintRepositoryProvider = Provider<InpaintRepository>(
(ref) {
final remoteDataSource = ref.watch(inpaintRemoteDataSourceProvider);
return InpaintRepositoryImpl(remoteDataSource);
},
);
4.3 Remote DataSource
// lib/features/inpaint/data/datasources/inpaint_remote_datasource.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/network/api_client.dart';
import '../../models/inpaint_task.dart';
class InpaintRemoteDataSource {
final ApiClient _apiClient;
InpaintRemoteDataSource(this._apiClient);
/// 去除水印
Future<InpaintTask> removeWatermark(File imageFile) async {
try {
// 构建 FormData
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: imageFile.path.split('/').last,
),
});
// 发送请求
final response = await _apiClient.post(
'/api/inpaint/remove',
data: formData,
);
// 解析响应
return InpaintTask.fromJson(response.data);
} on DioException catch (e) {
if (e.response?.statusCode == 402) {
throw ServerException('积分不足,请先充值');
} else if (e.response?.statusCode == 400) {
throw ServerException(e.response?.data['message'] ?? '请求参数错误');
} else if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
throw NetworkException('网络连接超时,请检查网络设置');
} else {
throw NetworkException('网络请求失败: ${e.message}');
}
} catch (e) {
throw ServerException('未知错误: $e');
}
}
/// 获取任务详情
Future<InpaintTask> getTask(String taskId) async {
try {
final response = await _apiClient.get('/api/inpaint/tasks/$taskId');
return InpaintTask.fromJson(response.data);
} on DioException catch (e) {
throw NetworkException('获取任务详情失败: ${e.message}');
}
}
/// 获取任务列表
Future<List<InpaintTask>> getTasks() async {
try {
final response = await _apiClient.get('/api/inpaint/tasks');
final List<dynamic> data = response.data as List;
return data.map((json) => InpaintTask.fromJson(json)).toList();
} on DioException catch (e) {
throw NetworkException('获取任务列表失败: ${e.message}');
}
}
}
/// Provider 定义
final inpaintRemoteDataSourceProvider = Provider<InpaintRemoteDataSource>(
(ref) {
final apiClient = ref.watch(apiClientProvider);
return InpaintRemoteDataSource(apiClient);
},
);
/// 异常类
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}
class ServerException implements Exception {
final String message;
ServerException(this.message);
}

五、结果展示页面
5.1 结果页 UI
// lib/features/inpaint/presentation/pages/result_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io';
import '../../../../app/app_colors.dart';
import '../../../../shared/widgets/gradient_button.dart';
import '../../models/inpaint_task.dart';
class ResultPage extends ConsumerStatefulWidget {
final InpaintTask task;
const ResultPage({required this.task, super.key});
ConsumerState<ResultPage> createState() => _ResultPageState();
}
class _ResultPageState extends ConsumerState<ResultPage> {
bool _showOriginal = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('处理结果'),
actions: [
IconButton(
icon: const Icon(Icons.share),
onPressed: _shareImage,
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
// 对比切换
_buildComparisonToggle(),
// 图片展示
_buildImageDisplay(),
const SizedBox(height: 24),
// 操作按钮
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GradientButton(
text: '下载图片',
onPressed: _downloadImage,
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.home),
label: const Text('返回首页'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.white,
side: const BorderSide(color: AppColors.gray700),
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
// 提示信息
_buildTips(),
],
),
),
);
}
Widget _buildComparisonToggle() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: _buildToggleButton(
label: '处理后',
isSelected: !_showOriginal,
onTap: () => setState(() => _showOriginal = false),
),
),
Expanded(
child: _buildToggleButton(
label: '原图对比',
isSelected: _showOriginal,
onTap: () => setState(() => _showOriginal = true),
),
),
],
),
);
}
Widget _buildToggleButton({
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? AppColors.purple : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected ? AppColors.white : AppColors.gray400,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
);
}
Widget _buildImageDisplay() {
if (widget.task.status != TaskStatus.completed) {
return _buildProcessingState();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _showOriginal
? _buildComparisonView()
: _buildResultImage(),
),
);
}
Widget _buildProcessingState() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
widget.task.status == TaskStatus.processing
? 'AI 正在处理中...'
: '等待处理',
style: const TextStyle(
color: AppColors.gray400,
fontSize: 16,
),
),
],
),
);
}
Widget _buildResultImage() {
if (widget.task.outputFile == null) {
return const SizedBox();
}
// 判断是网络图片还是本地图片
final isNetwork = widget.task.outputFile!.startsWith('http');
return isNetwork
? Image.network(
widget.task.outputFile!,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
)
: Image.file(
File(widget.task.outputFile!),
fit: BoxFit.contain,
);
}
Widget _buildComparisonView() {
return Row(
children: [
Expanded(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
color: AppColors.gray800,
child: const Text(
'原图',
style: TextStyle(
color: AppColors.white,
fontSize: 12,
),
),
),
Image.file(
File(widget.task.inputFile),
fit: BoxFit.contain,
),
],
),
),
Expanded(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
color: AppColors.purple,
child: const Text(
'处理后',
style: TextStyle(
color: AppColors.white,
fontSize: 12,
),
),
),
_buildResultImage(),
],
),
),
],
);
}
Widget _buildTips() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.green.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.check_circle_outline,
color: AppColors.green,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'处理完成!图片已保存到相册',
style: TextStyle(
color: AppColors.gray400,
fontSize: 14,
),
),
),
],
),
);
}
Future<void> _downloadImage() async {
// 下载逻辑
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('图片已保存到相册')),
);
}
Future<void> _shareImage() async {
// 分享逻辑
}
}

六、下载与分享
6.1 保存到相册
// lib/core/utils/image_saver.dart
import 'dart:io';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
class ImageSaver {
/// 保存图片到相册
static Future<bool> saveToGallery(String imagePath) async {
try {
// 请求存储权限
final status = await Permission.storage.request();
if (!status.isGranted) {
return false;
}
// 读取图片文件
final file = File(imagePath);
if (!file.existsSync()) {
return false;
}
final bytes = await file.readAsBytes();
// 保存到相册
final result = await ImageGallerySaver.saveImage(
bytes,
quality: 100,
name: 'cleanmark_${DateTime.now().millisecondsSinceEpoch}',
);
return result['isSuccess'] == true;
} catch (e) {
print('保存图片失败: $e');
return false;
}
}
}
6.2 分享功能
// lib/core/utils/share_helper.dart
import 'package:share_plus/share_plus.dart';
import 'dart:io';
class ShareHelper {
/// 分享图片
static Future<void> shareImage(String imagePath) async {
try {
final file = File(imagePath);
if (!file.existsSync()) {
throw Exception('文件不存在');
}
await Share.shareXFiles(
[XFile(imagePath)],
text: '使用 CleanMark AI 去除水印',
);
} catch (e) {
print('分享失败: $e');
rethrow;
}
}
}
七、错误处理
7.1 网络错误处理
// lib/core/errors/failures.dart
abstract class Failure {
final String message;
const Failure(this.message);
}
class NetworkFailure extends Failure {
const NetworkFailure([String message = '网络连接失败,请检查网络设置'])
: super(message);
}
class ServerFailure extends Failure {
const ServerFailure(String message) : super(message);
}
class ValidationFailure extends Failure {
const ValidationFailure(String message) : super(message);
}
class InsufficientCreditsFailure extends Failure {
const InsufficientCreditsFailure([String message = '积分不足,请先充值'])
: super(message);
}
class UnknownFailure extends Failure {
const UnknownFailure(String message) : super(message);
}
7.2 错误提示优化
// lib/shared/widgets/error_dialog.dart
class ErrorDialog extends StatelessWidget {
final String title;
final String message;
final VoidCallback? onRetry;
const ErrorDialog({
required this.title,
required this.message,
this.onRetry,
super.key,
});
static Future<void> show(
BuildContext context, {
required String title,
required String message,
VoidCallback? onRetry,
}) {
return showDialog(
context: context,
builder: (context) => ErrorDialog(
title: title,
message: message,
onRetry: onRetry,
),
);
}
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.cardDark,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(Icons.error_outline, color: AppColors.red),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(color: AppColors.white),
),
],
),
content: Text(
message,
style: const TextStyle(color: AppColors.gray400),
),
actions: [
if (onRetry != null)
TextButton(
onPressed: () {
Navigator.pop(context);
onRetry();
},
child: const Text('重试'),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
);
}
}
八、性能优化
8.1 图片压缩
// lib/core/utils/image_compressor.dart
import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
class ImageCompressor {
/// 压缩图片
static Future<File?> compress(File file) async {
try {
// 获取临时目录
final tempDir = await getTemporaryDirectory();
final targetPath = '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
// 压缩图片
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
targetPath,
quality: 85,
minWidth: 1920,
minHeight: 1920,
);
return result != null ? File(result.path) : null;
} catch (e) {
print('压缩图片失败: $e');
return null;
}
}
}
8.2 缓存优化
// 使用 cached_network_image 缓存网络图片
import 'package:cached_network_image/cached_network_image.dart';
Widget _buildResultImage() {
if (widget.task.outputFile == null) {
return const SizedBox();
}
final isNetwork = widget.task.outputFile!.startsWith('http');
return isNetwork
? CachedNetworkImage(
imageUrl: widget.task.outputFile!,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
: Image.file(
File(widget.task.outputFile!),
fit: BoxFit.contain,
);
}
8.3 上传进度显示
// lib/features/inpaint/presentation/widgets/upload_progress_dialog.dart
class UploadProgressDialog extends StatelessWidget {
final double progress;
const UploadProgressDialog({
required this.progress,
super.key,
});
static void show(BuildContext context, double progress) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => UploadProgressDialog(progress: progress),
);
}
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.cardDark,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(value: progress),
const SizedBox(height: 16),
Text(
'上传中... ${(progress * 100).toInt()}%',
style: const TextStyle(color: AppColors.white),
),
],
),
);
}
}
本篇完整小结
这篇文章我们完成了:
- ✅ 功能流程设计(选择 → 上传 → 处理 → 展示)
- ✅ 数据模型定义(InpaintTask)
- ✅ 图片上传页面 UI
- ✅ 状态管理(Riverpod Provider)
- ✅ UseCase 和 Repository 实现
- ✅ API 服务层(Dio + FormData)
- ✅ 结果展示页面(对比视图)
- ✅ 下载和分享功能
- ✅ 错误处理机制
- ✅ 性能优化(压缩、缓存)
关键要点:
- 采用 Clean Architecture 分层架构
- 使用 Riverpod 管理状态
- 使用 Either 类型处理错误
- FormData 上传文件到服务器
- 提供原图对比功能
- 图片压缩减少上传时间
- 使用缓存优化图片加载
API 调用流程:
UI Layer (ImageUploadPage)
↓
Provider (InpaintNotifier)
↓
UseCase (RemoveWatermarkUseCase)
↓
Repository (InpaintRepositoryImpl)
↓
DataSource (InpaintRemoteDataSource)
↓
API Client (Dio)
错误处理策略:
- 网络错误:提示检查网络,提供重试按钮
- 服务器错误:显示具体错误信息
- 积分不足:引导用户充值
- 文件验证:上传前验证格式和大小
思考题
- 为什么要使用 Either 类型而不是直接抛出异常?
- 如何实现上传进度的实时显示?
- 如果处理时间很长(超过 30 秒),如何优化用户体验?
下一篇预告:第10篇 - 视频去水印与进度管理
更多推荐


所有评论(0)