我们经常会遇到图片上传后本地回显的需求,大概流程如下:

  1. 拍照或选择图片,并压缩图片文件
  2. 将压缩后的图片上传到服务器,并得到服务端返回的图片 URL
  3. 客户端加载 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());
  }
}