从零开发小红书风格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', // 添加额外参数
})
性能优化点
- 分片上传:大文件分割为多个部分上传
- 断点续传:记录已上传进度
- 缓存策略:已上传图片本地缓存
- 并发控制:限制同时上传数量
最终效果
总结
通过这次开发,我深刻体会到移动端文件上传与Web的不同之处:
- 需要考虑更多设备兼容性问题
- 内存管理更为关键
- 用户体验要求更高
- 网络状态处理更复杂