# 从零开发小红书风格Flutter应用:图片上传功能实现踩坑记录

发布于:2025-04-16 ⋅ 阅读:(22) ⋅ 点赞:(0)

从零开发小红书风格Flutter应用:图片上传功能实现踩坑记录

作为第一次开发完整Flutter应用的经历,我在实现类似小红书的图片上传功能时遇到了不少挑战。本文将完整记录整个开发过程,包括技术选型、实现细节和遇到的问题。

技术栈选择

  • 前端框架:Flutter 3.16
  • 状态管理:原生State管理(考虑到功能相对简单)
  • 图片选择:image_picker插件
  • 图片预览:photo_view插件
  • 网络请求:Dio
  • 持久化存储:shared_preferences

核心功能实现

1. 多图选择与预览

// 图片选择逻辑
Future<void> _pickImages() async {
  try {
    final List<XFile> images = await _picker.pickMultiImage();
    
    if (images.isNotEmpty) {
      // 检查图片数量限制
      if (_images.length + images.length > 9) {
        throw Exception('最多只能上传9张图片');
      }

      // 检查图片大小
      for (var image in images) {
        final File file = File(image.path);
        final sizeInMb = (await file.length()) / (1024 * 1024);
        if (sizeInMb > 5) throw Exception('图片大小不能超过5MB');
      }

      setState(() {
        _images.addAll(images.map((image) => File(image.path)));
      });
    }
  } catch (e) {
    _showErrorSnackBar(e.toString());
  }
}

// 图片预览
void _openGallery(int initialIndex) {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => Scaffold(
        backgroundColor: Colors.black,
        body: Stack(
          children: [
            PhotoViewGallery.builder(
              itemCount: _images.length,
              builder: (context, index) => PhotoViewGalleryPageOptions(
                imageProvider: FileImage(_images[index]),
              pageController: PageController(initialPage: initialIndex),
            ),
            Positioned(
              top: 40,
              right: 20,
              child: IconButton(
                icon: Icon(Icons.close, color: Colors.white),
                onPressed: () => Navigator.pop(context),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

2. 图片上传逻辑

Future<void> _uploadBlog() async {
  if (!_validateForm()) return;

  setState(() => _isUploading = true);

  try {
    final token = await _getToken();
    final dioInstance = _initDioClient(token);

    // 上传图片
    List<Map<String, String>> imageUrls = [];
    for (var i = 0; i < _images.length; i++) {
      final response = await _uploadSingleImage(dioInstance, _images[i], i);
      imageUrls.add({'image': response['data']['url']});
    }

    // 提交博客内容
    await _submitBlogContent(dioInstance, imageUrls);
    
    if (mounted) {
      eventBus.fire(BlogCreatedEvent());
      context.go('/');
    }
  } catch (e) {
    _showErrorSnackBar('发布失败: ${e.toString()}');
  } finally {
    if (mounted) setState(() => _isUploading = false);
  }
}

Future<Map<String, dynamic>> _uploadSingleImage(
  dio.Dio dioInstance, 
  File image,
  int index
) async {
  String fileName = image.path.split('/').last;
  List<int> imageBytes = await image.readAsBytes();

  final formData = dio.FormData.fromMap({
    'file': await dio.MultipartFile.fromBytes(
      imageBytes,
      filename: fileName,
      contentType: MediaType('image', 'jpeg'),
    ),
  });

  final response = await dioInstance.post(
    '/upload/image',
    data: formData,
    onSendProgress: (sent, total) {
      setState(() {
        _uploadProgress = sent / total;
      });
    },
  );

  if (response.statusCode != 200 || !response.data['success']) {
    throw Exception('图片上传失败');
  }

  return response.data;
}

遇到的问题与解决方案

1. 图片上传卡顿

问题现象:选择多张图片后,上传过程中UI明显卡顿

原因分析

  • 同步读取多张大图导致主线程阻塞
  • 未做图片压缩处理
  • 进度更新过于频繁

解决方案

// 优化后的图片处理
Future<List<int>> _processImage(File image) async {
  // 1. 压缩图片
  final compressedImage = await FlutterImageCompress.compressWithFile(
    image.path,
    minWidth: 1080,
    quality: 85,
  );
  
  // 2. 使用Isolate处理
  return await compute(_compressInBackground, compressedImage);
}

List<int> _compressInBackground(List<int> image) {
  // 在后台线程执行压缩
  return image;
}

2. 安卓端图片路径问题

问题现象:在部分安卓设备上报错"FileNotFoundException"

解决方案

// 使用正确的路径获取方式
String getFileName(String path) {
  if (Platform.isAndroid) {
    return path.split('/').last;
  } else {
    return path.split('/').last.split('.').first;
  }
}

3. 服务器接收文件失败

问题排查

  • 检查Content-Type是否正确
  • 验证Multipart格式
  • 测试直接使用Postman上传

最终方案

// 修正后的FormData构造
dio.FormData.fromMap({
  'file': await dio.MultipartFile.fromFile(
    image.path,
    filename: fileName,
    contentType: MediaType('image', 'jpeg'),
  ),
  'type': 'blog_image',  // 添加额外参数
})

性能优化点

  1. 分片上传:大文件分割为多个部分上传
  2. 断点续传:记录已上传进度
  3. 缓存策略:已上传图片本地缓存
  4. 并发控制:限制同时上传数量

最终效果

上传流程演示

总结

通过这次开发,我深刻体会到移动端文件上传与Web的不同之处:

  1. 需要考虑更多设备兼容性问题
  2. 内存管理更为关键
  3. 用户体验要求更高
  4. 网络状态处理更复杂

网站公告

今日签到

点亮在社区的每一天
去签到