我们经常会遇到图片上传后本地回显的需求,大概流程如下:
- 拍照或选择图片,并压缩图片文件
- 将压缩后的图片上传到服务器,并得到服务端返回的图片 URL
- 客户端加载 URL 显示该图片
问题主要出在第三步
- 图片本来就是通过客户端上传的,没必要再花费一份流量从服务器中下载
- 加载远程图片可能缓慢,甚至会有失败的可能
解决方案也很简单,在图片上传成功后,我们拿到了该图片的 URL,此时我们就可以将 URL 作为 key,本地压缩后的图片作为 value 存储起来,直接绕过网络加载,这种方式能极大的提升用户体验。
很多图片加载框架都是支持预设缓存的,Android,Flutter 都有对应的方案, 而 ReactNative 可能需要自己写插件了。
Android Compose (Coil)
Coli 通过 ImageLoader 管理图片的加载和缓存,默认情况下全局使用单例的 SingletonImageLoader。具体步骤如下:
FooScreen.kt
@Composable
fun FooScreen(viewModel: FooViewModel = viewModel) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val imageLoader = remember { SingletonImageLoader.get(context) }
val pickImage = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
coroutineScope.launch {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val compressedFile = ImageUtil.compressFile(context, inputStream)
viewModel.uploadImage(
imageLoader = imageLoader,
compressedFile = compressedFile,
)
}
}
}
}
Column {
AsyncImage(
model = viewModel.imageUrl,
contentDescription = "Foo image",
modifier = Modifier
.size(200)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop,
)
Button("Upload Image") {
pickImage.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
}
}
}
FooViewModel.kt
class FooViewModel() : ViewModel() {
var imageUrl by mutableStateOf<String?>(null)
fun uploadImage(imageLoader: ImageLoader, compressedFile: File) {
viewModelScope.launch {
try {
val url = APi.uploadImage(compressedFile)
imageLoader.diskCache?.openEditor(firstImageUrl)?.let { editor ->
try {
compressedFile
.copyTo(target = editor.data.toFile(), overwrite = true)
editor.commit()
} catch (_: Exception) {
}
}
imageUrl = url
} catch(_: Exception) {
imageUrl = null
}
}
}
}
Flutter (cached_network_image)
cached_network_image 通过 DefaultCacheManager 管理图片缓存,但是 DefaultCacheManager 是简介依赖的,我们不能直接使用,需要先添加依赖:
flutter pub add flutter_cache_manager
另外需要注意的是 Flutter 中我们设置图片是通过 byte 数组进行的。
final comressedImage: Uint8List = ....
final url = await api.uploadImage(compressedImage);
await DefaultCacheManager().putFile(
url,
comressedImage,
fileExtension: "jpg",
);
// ----------------------------
CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
progressIndicatorBuilder: (context, url, progress) {
return Center(child: CircularProgressIndicator());
}
}