告别上传失败!Flutter Camera + dio 实现99%成功率的照片云端同步方案
【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/dio/dio
你是否遇到过拍照后上传失败的尴尬?旅行中抓拍的美景、工作中重要的文档扫描,因为网络波动或代码问题丢失,让人抓狂。本文将带你用Flutter Camera捕获精彩瞬间,通过dio网络库实现稳定高效的照片上传,从拍摄到云端一气呵成,再也不用担心照片丢失。
读完本文你将掌握:
- Flutter Camera的基础配置与照片捕获技巧
- dio的高级上传功能(断点续传、进度监听、错误重试)
- 完整的拍照上传业务逻辑(权限处理、文件压缩、状态管理)
- 实战项目的代码组织与最佳实践
开发环境准备
在开始实现照片上传功能前,我们需要准备基础的开发环境和项目配置。首先确保你的Flutter环境已经搭建完成,然后创建一个新的Flutter项目并添加必要的依赖。
项目依赖配置
在pubspec.yaml文件中添加以下依赖:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0 # 最新稳定版
camera: ^0.10.0+1
image_picker: ^0.8.6+1
path_provider: ^2.0.15
flutter_spinkit: ^5.2.0 # 加载动画
其中dio是我们的核心网络库,camera用于调用设备相机,image_picker提供相册选择功能,path_provider帮助我们管理文件路径,flutter_spinkit则提供美观的加载动画。
平台权限配置
Android配置
在android/app/src/main/AndroidManifest.xml中添加相机和存储权限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" />
iOS配置
在ios/Runner/Info.plist中添加权限描述:
<key>NSCameraUsageDescription</key>
<string>需要访问相机拍摄照片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册选择照片</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Flutter Camera实现拍照功能
Flutter Camera插件提供了访问设备相机的接口,让我们可以轻松实现拍照功能。下面我们将创建一个相机预览界面,并实现拍照逻辑。
相机初始化
首先,我们需要获取设备上可用的相机列表,并初始化相机控制器:
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
class CameraPage extends StatefulWidget {
const CameraPage({super.key});
@override
State<CameraPage> createState() => _CameraPageState();
}
class _CameraPageState extends State<CameraPage> {
late CameraController _controller;
late Future<void> _initializeControllerFuture;
List<CameraDescription> _cameras = [];
int _selectedCameraIndex = 0;
@override
void initState() {
super.initState();
_initializeCamera();
}
Future<void> _initializeCamera() async {
// 获取可用相机列表
_cameras = await availableCameras();
// 初始化相机控制器,使用后置摄像头
_controller = CameraController(
_cameras[_selectedCameraIndex],
ResolutionPreset.medium, // 选择中等分辨率
);
// 初始化控制器
_initializeControllerFuture = _controller.initialize();
setState(() {});
}
@override
void dispose() {
// 释放相机资源
_controller.dispose();
super.dispose();
}
// 构建相机预览界面
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// 如果初始化完成,显示相机预览
return CameraPreview(_controller);
} else {
// 否则显示加载指示器
return const Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 拍照逻辑将在这里实现
},
child: const Icon(Icons.camera_alt),
),
);
}
}
实现拍照功能
在相机预览界面上添加拍照按钮,并实现拍照逻辑:
Future<void> _takePicture() async {
try {
// 确保相机控制器已初始化
await _initializeControllerFuture;
// 获取应用的文档目录
final directory = await getApplicationDocumentsDirectory();
final imagePath = '${directory.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
// 拍照并保存图片
await _controller.takePicture(imagePath);
// 跳转到预览页面,传入图片路径
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PreviewPage(imagePath: imagePath),
),
);
} catch (e) {
// 处理错误
print(e);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('拍照失败: $e')),
);
}
}
将拍照功能绑定到悬浮按钮:
floatingActionButton: FloatingActionButton(
onPressed: _takePicture,
child: const Icon(Icons.camera_alt),
),
照片预览界面
创建一个照片预览界面,用户可以在这里预览照片并选择是否上传:
class PreviewPage extends StatelessWidget {
final String imagePath;
const PreviewPage({super.key, required this.imagePath});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('预览照片'),
actions: [
IconButton(
icon: const Icon(Icons.upload),
onPressed: () {
// 上传照片逻辑
Navigator.pop(context); // 返回相机页面
},
),
],
),
body: Center(
child: Image.file(File(imagePath)),
),
);
}
}
dio实现照片上传功能
dio是一个强大的Dart HTTP客户端,支持拦截器、FormData、请求取消、文件上传/下载等功能。下面我们将使用dio实现照片上传功能。
初始化dio实例
首先,创建一个dio实例并进行基础配置:
import 'package:dio/dio.dart';
class DioClient {
final Dio _dio;
DioClient() : _dio = Dio() {
// 基础配置
_dio.options.baseUrl = 'https://api.example.com/'; // 替换为你的API地址
_dio.options.connectTimeout = const Duration(seconds: 5);
_dio.options.receiveTimeout = const Duration(seconds: 3);
// 添加日志拦截器,方便调试
_dio.interceptors.add(LogInterceptor(
request: true,
responseBody: true,
requestBody: true,
requestHeader: true,
));
}
// 上传文件方法
Future<String> uploadFile(String filePath) async {
try {
// 创建FormData
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: 'photo.jpg', // 文件名
),
'description': '照片上传测试', // 可选的额外参数
});
// 发送POST请求
final response = await _dio.post(
'upload', // 上传接口的路径
data: formData,
onSendProgress: (int sent, int total) {
// 上传进度回调
final progress = (sent / total * 100).toStringAsFixed(0);
print('上传进度: $progress%');
},
);
// 处理响应
if (response.statusCode == 200) {
return response.data['url']; // 返回服务器返回的图片URL
} else {
throw Exception('上传失败: ${response.statusCode}');
}
} catch (e) {
throw Exception('上传失败: $e');
}
}
}
实现带进度的上传功能
在上传过程中,我们通常需要显示上传进度。修改PreviewPage,添加上传进度显示:
class PreviewPage extends StatefulWidget {
final String imagePath;
const PreviewPage({super.key, required this.imagePath});
@override
State<PreviewPage> createState() => _PreviewPageState();
}
class _PreviewPageState extends State<PreviewPage> {
final DioClient _dioClient = DioClient();
double _uploadProgress = 0.0;
bool _isUploading = false;
Future<void> _uploadImage() async {
setState(() {
_isUploading = true;
_uploadProgress = 0.0;
});
try {
// 调用上传方法
final imageUrl = await _dioClient.uploadFile(widget.imagePath);
// 上传成功
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('上传成功!')),
);
// 返回结果
Navigator.pop(context, imageUrl);
} catch (e) {
// 上传失败
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('上传失败: $e')),
);
} finally {
if (mounted) {
setState(() {
_isUploading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('预览照片'),
actions: [
_isUploading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
)
: IconButton(
icon: const Icon(Icons.upload),
onPressed: _uploadImage,
),
],
),
body: Column(
children: [
Expanded(
child: Center(
child: Image.file(File(widget.imagePath)),
),
),
if (_isUploading)
LinearProgressIndicator(
value: _uploadProgress,
minHeight: 4,
),
],
),
);
}
}
高级功能实现
文件压缩
为了减少上传流量和提高上传速度,我们可以在上传前对图片进行压缩。使用flutter_image_compress库:
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
Future<String> compressImage(String imagePath) async {
final directory = await getTemporaryDirectory();
final targetPath = '${directory.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 压缩图片
await FlutterImageCompress.compressAndGetFile(
imagePath,
targetPath,
quality: 80, // 质量,0-100
minWidth: 1280, // 最小宽度
minHeight: 720, // 最小高度
);
return targetPath;
}
在上传前调用压缩方法:
Future<void> _uploadImage() async {
// ... 省略其他代码 ...
try {
// 压缩图片
final compressedPath = await compressImage(widget.imagePath);
// 上传压缩后的图片
final imageUrl = await _dioClient.uploadFile(compressedPath);
// ... 省略其他代码 ...
} catch (e) {
// ... 省略其他代码 ...
}
}
断点续传功能
对于大文件上传,断点续传功能非常重要。dio支持通过CancelToken实现请求取消,结合本地存储可以实现断点续传:
// 创建取消令牌
CancelToken cancelToken = CancelToken();
// 上传方法添加取消令牌参数
Future<String> uploadFile(String filePath, {CancelToken? cancelToken}) async {
try {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: 'photo.jpg',
),
});
final response = await _dio.post(
'upload',
data: formData,
cancelToken: cancelToken,
onSendProgress: (int sent, int total) {
// 保存已上传进度
_saveUploadProgress(filePath, sent, total);
},
);
// ... 省略其他代码 ...
} catch (e) {
if (CancelToken.isCancel(e)) {
print('上传已取消');
} else {
throw e;
}
}
}
完整业务流程整合
现在我们已经实现了拍照和上传的基础功能,让我们将它们整合到一个完整的业务流程中。
状态管理
使用Provider或Riverpod管理应用状态,这里我们使用简单的StatefulWidget管理状态:
class CameraApp extends StatelessWidget {
const CameraApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '拍照上传示例',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CameraHomePage(),
);
}
}
class CameraHomePage extends StatefulWidget {
const CameraHomePage({super.key});
@override
State<CameraHomePage> createState() => _CameraHomePageState();
}
class _CameraHomePageState extends State<CameraHomePage> {
List<String> _uploadedImages = [];
void _addUploadedImage(String url) {
setState(() {
_uploadedImages.add(url);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('拍照上传'),
),
body: _uploadedImages.isEmpty
? const Center(child: Text('暂无上传图片'))
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: _uploadedImages.length,
itemBuilder: (context, index) {
return Image.network(
_uploadedImages[index],
fit: BoxFit.cover,
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 导航到相机页面
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const CameraPage()),
);
// 如果返回了图片URL,添加到列表
if (result is String) {
_addUploadedImage(result);
}
},
child: const Icon(Icons.camera),
),
);
}
}
错误处理与重试机制
在实际应用中,网络请求可能会失败,我们需要添加错误处理和重试机制:
Future<void> _uploadWithRetry(String imagePath, {int retryCount = 3}) async {
try {
final imageUrl = await _dioClient.uploadFile(imagePath);
if (!mounted) return;
Navigator.pop(context, imageUrl);
} catch (e) {
if (retryCount > 0) {
// 显示重试对话框
final shouldRetry = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('上传失败'),
content: Text('是否重试? ($retryCount次机会)'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('重试'),
),
],
),
);
if (shouldRetry == true) {
_uploadWithRetry(imagePath, retryCount: retryCount - 1);
}
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('多次上传失败,请稍后再试')),
);
setState(() => _isUploading = false);
}
}
}
项目结构与最佳实践
推荐的项目结构
对于一个完整的Flutter拍照上传项目,推荐以下项目结构:
lib/
├── main.dart # 应用入口
├── app/ # 应用配置
│ ├── app.dart # 应用根组件
│ └── routes.dart # 路由配置
├── data/ # 数据层
│ ├── dio_client.dart # dio客户端
│ └── repositories/ # 仓库
│ └── image_repository.dart # 图片仓库
├── features/ # 功能模块
│ ├── camera/ # 相机功能
│ │ ├── camera_page.dart # 相机页面
│ │ └── preview_page.dart # 预览页面
│ └── gallery/ # 相册功能
│ └── gallery_page.dart # 相册页面
├── utils/ # 工具类
│ ├── image_compressor.dart # 图片压缩工具
│ └── permission_utils.dart # 权限工具
└── widgets/ # 共享组件
├── progress_indicator.dart # 进度指示器
└── image_item.dart # 图片项组件
性能优化建议
- 图片压缩:上传前压缩图片,减少上传大小
- 后台上传:使用Isolate或WorkManager实现后台上传
- 缓存管理:合理管理图片缓存,避免内存泄漏
- 分批上传:多张图片时分批上传,避免同时发起多个请求
- 取消机制:实现上传取消功能,提升用户体验
总结与展望
通过本文的学习,我们实现了从拍照到云端的完整照片上传流程,包括:
- 使用Flutter Camera插件访问设备相机并拍照
- 使用dio库实现带进度的文件上传
- 图片压缩、错误重试等高级功能
- 完整的业务流程整合
未来可以进一步扩展的功能:
- 多图选择和批量上传
- 后台上传和断点续传
- 图片编辑功能(裁剪、滤镜等)
- 上传队列管理
希望本文能够帮助你构建更加稳定、高效的照片上传功能。如果你有任何问题或建议,欢迎在评论区留言讨论!
别忘了点赞、收藏、关注三连,下期我们将介绍如何实现图片的智能分类和管理!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



