Coil-Compose 接入指南

3,987 阅读4分钟

关注微信公众号:Jetpack Compose 博物馆,加入Compose中文技术社区获取更多优质技术教程! Coil 是一款基于 Kotlin 协程的 Android 图片加载库,Coil 的名字由 Coroutine Image Loader 首字母构成,Coil 具有以下几个优势。

  1. 更快:Coil 在性能优化上做了许多工作,包括内存与磁盘缓存策略,将网络图片进行降采样存放,自动化暂停或取消图片的网络请求等。
  2. 更轻量:Coil 只会为应用增加 2000 个方法(假设应用已经接入了 Okhttp 与 Coroutines),跟 Picasso 相当,相比 Glide 和 Fresco 要轻量非常多。
  3. 更简单:Coil 的 API 设计充分利用 Kotlin 语言的各种特性,简化和减少了许多样板代码。
  4. 更先进:Coil 秉承着 kotlin-first 原则,并且使用了许多目前非常流行的组件库,包括 Coroutines,Okhttp,Okio 与 AndroidX Lifecycles 等。

配置依赖

首先我们需要在 build.gradle(app) 中配置 Compose 版本的 Coil 组件库依赖。

implementation("io.coil-kt:coil-compose:$coil_version")

AsyncImage

我们可以使用 AsyncImage 组件展示一张网络图片,Coil 内部会帮助我们异步完成图片请求与渲染展示。AsyncImage 组件与我们最常使用的 Image 组件的参数功能相类似,并且还允许我们设置在网络请求后在 placeholder/error/fallback 不同状态的占位图,以及还可以设置 onLoading/onSuccess/onError 等不同事件的回调监听。 AsyncImage 组件简单易用,model 字段可以直接设置为网络图片 URL。

AsyncImage(
  model = "https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png",
  contentDescription = stringResource(R.string.description)
)

model 字段也可以设置为一个 ImageRequest 实例。通过 ImageRequest 我们可以设置更详细的异步请求配置。例如我们可以设置网络图片加载的淡入淡出效果,图片装载到内存时的像素尺寸,磁盘与内存的缓存策略,HTTP 请求头信息设置等等。当然如果你这些都不关心,只需使用 data 方法设置网络图片 URL,这与上面直接传入网络图片 URL的方式是等价的。 这里我们使用 crossfade 来设置网络图片加载的淡入效果,

AsyncImage(
  model = ImageRequest.Builder(LocalContext.current)
    .data("https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png")
    .crossfade(true)
    .build(),
  contentDescription = stringResource(R.string.description),
  placeholder = painterResource(id = R.drawable.place_holder),
  error = painterResource(id = R.drawable.error),
  onSuccess = {
    Log.d(TAG, "success")
  }
)

SubcomposeAsyncImage

SubcomposeAsyncImage 组件是 AsyncImage 组件的变种,具有更为灵活的 API 设计。例如使用 AsyncImage 组件我们是无法自定义加载动画的,只能设置成一张静态占位图。

SubcomposeAsyncImage(
  model = "https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png",
  loading = {
    CircularProgressIndicator() // 圆形进度条
  },
  contentDescription = stringResource(R.string.description)
)

除此之外,你可以使用带有 content 参数的重载方法,根据图像加载状态定制更为复杂的加载逻辑。SubcomposeAsyncImageContent() 是实际展示图片内容的组件,我们需要在加载完成后将其展示出来。

SubcomposeAsyncImage(
  model = "https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png",
  contentDescription = stringResource(R.string.description)
) {
  if (painter.state is AsyncImagePainter.State.Loading || painter.state is AsyncImagePainter.State.Error) {
    CircularProgressIndicator()
  } else {
    SubcomposeAsyncImageContent()
  }
}

实际上,如果我们没有主动设置图片装载到内存时的像素尺寸,SubcomposeAsyncImage 组件会默认根据当前组件布局的约束空间来确定图片最终装载的尺寸,这说明在图片装载前我们就需要预先获取当前 SubcomposeAsyncImage 组件的约束信息。在之前的文章中我们曾提到过,使用 SubcomposeLayout 组件可以使一个子组件在合成前获取到父组件的约束信息或其他组件测量信息的。可以看出 SubcomposeAsyncImage 组件其实就是依靠 SubcomposeLayout 提供的能力进行实现的。这里的子组件可以看作是我们传入的 content 内容,他会在 SubcomposeAsyncImage 组件测量时进行合成。 当然如果我们主动设置了图片装载到内存时的像素尺寸,那么 SubcomposeAsyncImage 组件内部会直接采用 Layout 方案实现,因为此时我们并不需要当前组件布局的约束信息。

SubcomposeAsyncImage(
  model = ImageRequest
    .Builder(LocalContext.current)
    .data("https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png")
    .size(1920, 1080) // 设置像素尺寸
    .build(),
  contentDescription = stringResource(R.string.description)
) {
  if (painter.state is AsyncImagePainter.State.Loading || painter.state is AsyncImagePainter.State.Error) {
    CircularProgressIndicator()
  } else {
    SubcomposeAsyncImageContent()
  }
}

AsyncImagePainter

实际上无论是 AsyncImage 组件还是 SubcomposeAsyncImage 组件,他们内部其实都是使用AsyncImagePainter 来完成异步网络请求与渲染的。如果你在工程中被限制不能使用 AsyncImage 组件,可以选用 AsyncImagePainter 与 Image 组件的搭配方案。

val painter = rememberAsyncImagePainter(
  model = ImageRequest.Builder(LocalContext.current)
    .data("https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png")
    .build()
)
if (painter.state is AsyncImagePainter.State.Loading) {
  CircularProgressIndicator()
}
Image(
  painter = painter,
  contentDescription = stringResource(R.string.description)
)

值得注意的是,只有当第一次 onDraw 时,AsyncImagePainter 才会进行异步网络请求获取图像,这是因为第一次 onDraw 会根据 drawContext 信息为 ImageLoader 提供 sizeResolver,用来确定图片装载到内存时的像素尺寸,感兴趣的同学可以自行查阅源码。所以我们不能根据 painter.state 是否为 Success 状态来决定是否合成 Image,因为只有 Image 被合成绘制时才会进行网络请求,否则会一直处于 Loading 状态。

val painter = rememberAsyncImagePainter(
  model = ImageRequest.Builder(LocalContext.current)
    .data("https://pic-go-bed.oss-cn-beijing.aliyuncs.com/img/20220316151929.png")
    .build()
)
// 错误示例,图片不能被成功加载。
if (painter.state is AsyncImagePainter.State.Success) {
  Image(
    painter = painter,
    contentDescription = stringResource(R.string.description)
  )
}

AsyncImagePainter 是一个底层 API,在使用时会出现很多诸如此类的非不可预期的错误行为,需要我们花费大量时间进行排查,所以建议还是尽可能的使用 AsyncImage 与 SubcomposeAsyncImage 这类配套的上层组件。