20、扩展函数与高阶函数在图片加载中的应用:Glide/Coil的扩展函数、图片加载的DSL配置、占位图与错误图的简化
图片加载,几乎是每个 Android 应用都绕不开的坎。我记得刚入行那会儿,项目里用的还是老牌的 Picasso,每次加载图片都要写一堆重复的配置代码——占位图、错误图、圆角、裁剪……一个页面下来,光图片加载的代码就占了半屏。后来接触了 Kotlin 的扩展函数和高阶函数,我才发现,原来图片加载可以写得这么优雅。
今天我们就来聊聊,如何用扩展函数和高阶函数,把 Glide 和 Coil 的图片加载代码,写得像 DSL 一样简洁。
为什么图片加载需要扩展函数?
先看一段典型的 Glide 调用代码:
Glide.with(context)
.load(url)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.circleCrop()
.into(imageView)
这段代码本身已经很清晰了,但问题在于——重复。如果你的项目里有几十个地方都要加载圆形头像,每个地方都得写 .placeholder()、.error()、.circleCrop(),那维护起来就是一场噩梦。
扩展函数能做什么?说白了,就是把「重复的逻辑」封装起来,让你只写一次。
核心思路: 用扩展函数给 ImageView 添加「加载图片」的能力,用高阶函数让调用方可以灵活定制配置。
Glide 扩展函数实战
我个人习惯的做法是,先定义一个基础的扩展函数,把 Glide 的通用配置封装进去:
fun ImageView.load(url: String?, config: GlideRequest<Drawable>.() -> GlideRequest<Drawable> = { this }) {
Glide.with(context)
.load(url)
.config()
.into(this)
}
这里的关键是 config: GlideRequest<Drawable>.() -> GlideRequest<Drawable>——它是一个高阶函数,接收者是 GlideRequest,返回值也是 GlideRequest。调用方可以在 lambda 里任意修改配置,比如加圆角、加占位图。
然后我们再封装几个常用的「快捷方式」:
fun ImageView.loadCircle(url: String?) {
load(url) {
placeholder(R.drawable.avatar_placeholder)
error(R.drawable.avatar_error)
circleCrop()
}
}
fun ImageView.loadRound(url: String?, radius: Int = 8) {
load(url) {
placeholder(R.drawable.img_placeholder)
error(R.drawable.img_error)
transform(RoundedCorners(radius))
}
}
你看,调用方只需要写一行:
imageView.loadCircle(user.avatarUrl)
imageView.loadRound(post.coverUrl, 12)
嗯,这里要注意:loadCircle 和 loadRound 内部复用了同一个 load 函数,只是传入了不同的配置 lambda。这就是高阶函数的威力——把「变化的部分」交给调用方,把「不变的部分」留在封装里。
小技巧: 如果你项目中同时用了 Glide 和 Coil,可以定义一个统一的接口,然后用扩展函数分别实现。这样上层业务代码完全不用关心底层用的是哪个库。
Coil 的扩展函数写法
Coil 本身是基于 Kotlin 协程的,它的 API 设计已经比较简洁了。但扩展函数依然有用武之地:
fun ImageView.loadCoil(url: String?, config: ImageRequest.Builder.() -> Unit = {}) {
val request = ImageRequest.Builder(context)
.data(url)
.target(this)
.apply(config)
.build()
context.imageLoader.enqueue(request)
}
调用方式跟 Glide 版本几乎一样:
imageView.loadCoil(url) {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
transformations(CircleCropTransformation())
}
我在项目中遇到过一个问题:Coil 的 placeholder 和 error 默认是 Int 类型的资源 ID,但有时候我们需要传 Drawable 对象。这时候扩展函数就可以做一层适配:
fun ImageView.loadCoil(url: String?, placeholder: Drawable? = null, error: Drawable? = null) {
loadCoil(url) {
placeholder?.let { placeholder(it) }
error?.let { error(it) }
}
}
这样调用方就可以直接传 Drawable 了,不用再操心资源 ID 的问题。
图片加载的 DSL 配置
如果你觉得上面的写法还不够「DSL」,那我们可以再进一步。用高阶函数构建一个图片加载的 DSL 上下文:
class ImageLoaderConfig {
var url: String? = null
var placeholder: Int? = null
var error: Int? = null
var isCircle: Boolean = false
var cornerRadius: Int = 0
var crossFade: Boolean = true
}
fun ImageView.load(config: ImageLoaderConfig.() -> Unit) {
val cfg = ImageLoaderConfig().apply(config)
// 根据 cfg 的属性决定用 Glide 还是 Coil,以及如何配置
Glide.with(context)
.load(cfg.url)
.apply {
cfg.placeholder?.let { placeholder(it) }
cfg.error?.let { error(it) }
if (cfg.isCircle) circleCrop()
if (cfg.cornerRadius > 0) transform(RoundedCorners(cfg.cornerRadius))
if (cfg.crossFade) transition(DrawableTransitionOptions.withCrossFade())
}
.into(this)
}
调用方现在可以这样写:
imageView.load {
url = user.avatarUrl
placeholder = R.drawable.avatar_placeholder
error = R.drawable.avatar_error
isCircle = true
crossFade = true
}
是不是感觉像在写配置脚本?这就是 DSL 的魅力——把复杂的 API 调用,变成声明式的配置。
避坑指南: 我曾经在 DSL 配置里忘了设置 url,结果图片加载不出来,排查了半天才发现是 url 为空。建议在 DSL 的 build 方法里加一个校验,如果 url 为空直接抛异常,或者打印警告日志。
占位图与错误图的简化
占位图和错误图,是图片加载中最容易「写脏」的地方。每个页面可能都有不同的占位图,但大部分时候它们只是重复的模板代码。
我的做法是:定义一个全局的默认配置,然后在扩展函数里自动应用:
object ImageDefaults {
var defaultPlaceholder: Int = R.drawable.ic_placeholder
var defaultError: Int = R.drawable.ic_error
var defaultCrossFade: Boolean = true
}
fun ImageView.loadWithDefaults(url: String?, config: GlideRequest<Drawable>.() -> GlideRequest<Drawable> = { this }) {
Glide.with(context)
.load(url)
.placeholder(ImageDefaults.defaultPlaceholder)
.error(ImageDefaults.defaultError)
.apply {
if (ImageDefaults.defaultCrossFade) transition(DrawableTransitionOptions.withCrossFade())
}
.config()
.into(this)
}
这样,大部分场景下你只需要写:
imageView.loadWithDefaults(url)
占位图和错误图自动就有了。如果某个页面需要特殊的占位图,再通过 config lambda 覆盖即可。
为什么会这样设计?因为 80% 的图片加载场景,占位图和错误图都是统一的。只有少数特殊页面(比如用户头像、商品详情图)才需要定制。把「通用」和「特殊」分开,代码自然就干净了。
知识体系总览
下面这张图,概括了本章的核心逻辑:
总结
扩展函数和高阶函数,说白了就是 Kotlin 给 Android 开发者的两把「瑞士军刀」。在图片加载这个场景里,它们帮我们做到了三件事:
- 消除重复:把 Glide/Coil 的通用配置封装成扩展函数,一行代码搞定图片加载。
- 灵活定制:用高阶函数把「变化的部分」暴露给调用方,想加圆角就加圆角,想换占位图就换占位图。
- DSL 化:构建一个图片加载的配置上下文,让代码读起来像配置文件,而不是 API 调用链。
你想想看,如果每个图片加载的地方都写五六行配置代码,一个项目下来得有多少冗余?用扩展函数和高阶函数,不仅代码量减半,可读性和可维护性也大幅提升。嗯,这就是 Kotlin 的魅力所在。
一句话总结: 扩展函数帮你「少写代码」,高阶函数帮你「灵活配置」,两者结合,图片加载的代码就能写得既简洁又强大。
公众号:蓝海资料掘金营,微信 deep3321