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)

嗯,这里要注意:loadCircleloadRound 内部复用了同一个 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 的 placeholdererror 默认是 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% 的图片加载场景,占位图和错误图都是统一的。只有少数特殊页面(比如用户头像、商品详情图)才需要定制。把「通用」和「特殊」分开,代码自然就干净了。

知识体系总览

下面这张图,概括了本章的核心逻辑:

图片加载扩展函数与DSL知识体系 扩展函数 + 高阶函数 Glide 扩展函数封装 load() / loadCircle() / loadRound() Coil 扩展函数封装 loadCoil() / 协程支持 DSL 配置:ImageLoaderConfig 占位图/错误图全局默认值 + 按需覆盖

总结

扩展函数和高阶函数,说白了就是 Kotlin 给 Android 开发者的两把「瑞士军刀」。在图片加载这个场景里,它们帮我们做到了三件事:

  • 消除重复:把 Glide/Coil 的通用配置封装成扩展函数,一行代码搞定图片加载。
  • 灵活定制:用高阶函数把「变化的部分」暴露给调用方,想加圆角就加圆角,想换占位图就换占位图。
  • DSL 化:构建一个图片加载的配置上下文,让代码读起来像配置文件,而不是 API 调用链。

你想想看,如果每个图片加载的地方都写五六行配置代码,一个项目下来得有多少冗余?用扩展函数和高阶函数,不仅代码量减半,可读性和可维护性也大幅提升。嗯,这就是 Kotlin 的魅力所在。

一句话总结: 扩展函数帮你「少写代码」,高阶函数帮你「灵活配置」,两者结合,图片加载的代码就能写得既简洁又强大。


公众号:蓝海资料掘金营,微信 deep3321