告别上传失败!Flutter Camera + dio 实现99%成功率的照片云端同步方案

告别上传失败!Flutter Camera + dio 实现99%成功率的照片云端同步方案

【免费下载链接】dio 【免费下载链接】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       # 图片项组件

性能优化建议

  1. 图片压缩:上传前压缩图片,减少上传大小
  2. 后台上传:使用Isolate或WorkManager实现后台上传
  3. 缓存管理:合理管理图片缓存,避免内存泄漏
  4. 分批上传:多张图片时分批上传,避免同时发起多个请求
  5. 取消机制:实现上传取消功能,提升用户体验

总结与展望

通过本文的学习,我们实现了从拍照到云端的完整照片上传流程,包括:

  • 使用Flutter Camera插件访问设备相机并拍照
  • 使用dio库实现带进度的文件上传
  • 图片压缩、错误重试等高级功能
  • 完整的业务流程整合

未来可以进一步扩展的功能:

  • 多图选择和批量上传
  • 后台上传和断点续传
  • 图片编辑功能(裁剪、滤镜等)
  • 上传队列管理

希望本文能够帮助你构建更加稳定、高效的照片上传功能。如果你有任何问题或建议,欢迎在评论区留言讨论!

别忘了点赞、收藏、关注三连,下期我们将介绍如何实现图片的智能分类和管理!

【免费下载链接】dio 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/dio/dio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值