Appearance
Landscapist:Compose 图片加载封装
回到总览:图片加载 / Image Loading相关模块:Compose 状态模型(重组与
remember语义)· Bitmap 内存与 GC(图片降采样 /inBitmap复用)· 性能与 jank(图片加载对帧率的影响)
一句话定义
Landscapist 是 Jetpack Compose 的图片加载库(skydoves 维护),在 Glide / Coil / Fresco 之上提供统一的 Composable API(GlideImage / CoilImage / FrescoImage,以及统一的 LandscapistImage),并把占位、失败、动画、模糊、调色板等能力做成声明式插件;其 Coil 3 后端为 KMP 原生,可在 Android / iOS / macOS / JVM / Wasm 复用同一调用。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| Landscapist Compose 图片加载 | Android / Compose 真机 lab:同一张图用 GlideImage / CoilImage + 原生 BitmapFactory 解码,演示约束驱动采样与引擎缓存清理 | ImageLoadingDemoActivity.kt · lab README |
1. 实现原理
1.1 统一 Composable API 与后端抽象
Landscapist 把三个引擎的差异收敛到同一组 Composable 与同一套请求模型 imageModel = { url }:
GlideImage→ 走 Glide 的RequestManagerCoilImage→ 走 Coil 的ImageLoaderFrescoImage→ 走 Fresco 的DraweeControllerLandscapistImage→ newer 统一入口,配合imageOptions { }与组件插槽
调用方只写 Compose 代码,不接触各引擎的 into() / Target 等命令式 API。
1.2 与 Compose 生命周期 / 尺寸的绑定(约束驱动采样)
这是它和「View 时代 Glide into(ImageView)」的本质区别:
- 生命周期绑定到 Composition:图片组件进入组合时发起请求,离开组合(
DisposeEffect)时取消请求并释放资源,避免 Activity/Fragment 销毁后还在解码。 - 约束驱动采样:Compose 把
Constraints(来自Modifier.size/ 父布局)作为目标尺寸传给组件,Landscapist 据此向底层引擎请求降采样到显示尺寸的 bitmap,而不是先加载原图再缩放。这一步直接决定内存占用(与02的inBitmap复用同源)。
1.3 插件架构
占位 / 失败 / 动画 / 模糊 / 调色板 / 圆角 / 缩放等能力以组件插件形式挂载,而非散落在业务代码:
kotlin
CoilImage(
imageModel = { "https://example.com/photo.jpg" },
modifier = Modifier.size(200.dp),
component = ImageComponentBuilder.new {
+BlurPlugin(blurRadius = 12) // 模糊
+PalettePlugin { palette -> // 取主色
val swatch = palette?.dominantSwatch
// swatch?.rgb
}
+ShimmerPlugin(baseColor = Color.LightGray) // 骨架闪烁
},
)插件在「请求前 / 成功 / 失败」各阶段插入逻辑,组件本身保持声明式。
1.4 缓存与内存来自底层引擎
Landscapist 自身主要做编排,不重新实现缓存:
- Glide 后端:Bitmap 复用走 Glide
BitmapPool(inBitmap),内存缓存LruResourceCache+ 磁盘 LRU。 - Coil 后端:内存
MemoryCache+ 磁盘DiskCache。 - Fresco 后端:含
native内存的CloseableReference体系。
因此「Bitmap 复用、内存上限、淘汰策略」这些治理能力来自引擎,Landscapist 只是把结果画到 Compose DrawScope。
各引擎内部加载原理(Glide 的 Request 链与四级查找、Coil 的
ImageLoader+ Interceptor 责任链、Fresco 的 Drawee + ImagePipeline producers、native 内存)见 02-图片加载方式总览与原理。
1.5 KMP 支持(Coil 3 后端)
Coil 3 是 KMP 原生的 ImageLoader;Landscapist 的 Coil 后端在 Coil 3 起可在 Android / iOS / macOS / JVM / Wasm 共用同一套 CoilImage 调用,差异只是各平台默认的 ImageLoader 与网络栈。
2. 特点与优势
2.1 它解决什么
把「请求生命周期管理 + 约束驱动采样 + 占位/失败/动画等横切关注点」从业务 Composable 里抽离,集中到库与插件中;同一组声明式 API 背后可换引擎、跨平台。
2.2 与其他实现方式 / 概念的区别
- vs 直接用 Coil 3 的
AsyncImage:AsyncImage是单后端、Compose 原生的轻量原语,足够覆盖大多数 Android + Compose 场景;Landscapist 额外提供多后端统一 API + 插件 DSL(blur/palette/shimmer/resize/circular) 。若只用 Coil 且不需要这些插件,Landscapist 是可选而非必需。 - vs 在 Compose 里用
AndroidView { Glide.with(it).load(url).into(it) }:AndroidView桥接回 View 系统,生命周期与尺寸要手动对齐 Compose;Landscapist 是 Compose 原生,自动绑定组合生命周期与约束,避免桥接样板与生命周期错位。 - vs Glide / Fresco 在 View 时代的
ImageView:引擎成熟度(Glide 的inBitmap池化、Fresco 的 native 内存)被保留,但调用面变成 Compose 友好的声明式组件,无需手动管理Target/ 取消。 - vs 手写「占位 + 淡入 + 失败重试」:插件把这套逻辑写成可复用组件,业务侧只声明所需插件,减少重复与出错面。
- 与 Koin 委托属性的回扣(边界) :图片加载是「随重组可能重算」的取值,用 Composable 的状态(
remember/derivedStateOf)比属性委托的「首次缓存」更合适——和 Koin 4.0 弃用by inject()改用koinInject()是同一类判断(见 委托属性)。
3. 用法
3.1 依赖与后端选择
kotlin
// 选一个后端即可(版本以官方最新为准;坐标前缀是 com.github.skydoves 而非 io.github)
implementation("com.github.skydoves:landscapist-glide:<version>")
implementation("com.github.skydoves:landscapist-coil:<version>")
implementation("com.github.skydoves:landscapist-fresco:<version>")
// 插件按需引入:landscapist-blur / -palette / -shimmer / -animation / -transformations3.2 基础用法(加载 / 占位 / 失败)
kotlin
GlideImage(
imageModel = { "https://example.com/photo.jpg" },
modifier = Modifier
.size(128.dp)
.clip(RoundedCornerShape(8.dp)),
loading = {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
},
failure = { error ->
Text("加载失败: ${error?.message}")
},
previewPlaceholder = painterResource(R.drawable.preview), // 预览占位
)3.3 插件(模糊 / 调色板 / 骨架 / 圆角)
kotlin
CoilImage(
imageModel = { url },
modifier = Modifier.size(200.dp),
component = ImageComponentBuilder.new {
+BlurPlugin(blurRadius = 12)
+PalettePlugin { palette -> /* 取 dominantSwatch.rgb 做背景 */ }
+ShimmerPlugin(baseColor = Color.LightGray)
},
)圆角 / 缩放等也可通过 Modifier.clip() 或 transformations 插件处理,不必回到命令式引擎 API。
3.4 Coil 3 后端 + KMP
在 commonMain 直接调用 CoilImage,各平台提供默认 ImageLoader 即可共用同一 UI 代码;差异只在平台网络/缓存实现,对业务 Composable 透明。
3.5 注意点(版本钉死)
landscapist-glide 内部钉死 Glide 4.16.0;若你的项目其它地方用了不同 Glide 版本,需 exclude 或对齐版本,否则会出现重复 Glide 类或方法签名冲突。
本页回答的问题(一句话答)
- Landscapist 和直接用 Coil 3 的
AsyncImage有什么区别?- 一句话答:
AsyncImage是单后端轻量原语;Landscapist 多后端统一 API + 插件 DSL(blur/palette/shimmer),并借 Coil 3 后端打通 KMP。只用 Coil 且不需插件时不必引入。
- 一句话答:
- 「约束驱动采样」解决什么?
- 一句话答:按 Compose 布局约束把图片降采样到显示尺寸再解码,减少 bitmap 内存与 OOM 风险;生命周期同时绑定到组合。
- Bitmap 复用 / 内存缓存是谁提供的?
- 一句话答:来自底层引擎(Glide
BitmapPool/inBitmap、CoilMemoryCache、Fresco native);Landscapist 只做编排与绘制。
- 一句话答:来自底层引擎(Glide
landscapist-glide有什么版本坑?- 一句话答:内部钉死 Glide 4.16.0,项目若用其它 Glide 版本需
exclude或对齐,避免类冲突。
- 一句话答:内部钉死 Glide 4.16.0,项目若用其它 Glide 版本需
对应实验
- Android / Compose 真机 lab:ImageLoadingDemoActivity.kt + lab README。同一张网络图用
GlideImage/CoilImage+ 原生BitmapFactory解码加载,演示约束驱动采样与引擎缓存清理;Fresco 因 native 依赖链较重、本机代理下不易拉全,runnable lab 暂不含,原理见 02 / 03。
复习检查题
- Landscapist 相比直接用 Coil 3 的
AsyncImage,多提供了什么?什么情况下其实不需要 Landscapist? 答:多提供「多后端统一 Composable + 插件 DSL(blur/palette/shimmer/resize/circular)」与 KMP 故事。若只用 Coil 且不需要这些插件,AsyncImage更轻,不必引入 Landscapist。 - 为什么说 Landscapist 的「约束驱动采样」对内存很关键? 答:它按 Compose 布局约束把图片降采样到显示尺寸再解码,避免加载远大于屏幕的原图 bitmap,直接降低内存峰值与 OOM 概率;请求生命周期还绑定到组合,离开即取消。
- Landscapist 自身实现 bitmap 复用和内存缓存吗? 答:不实现。复用与缓存来自底层引擎(Glide
BitmapPool/inBitmap、CoilMemoryCache、Fresco native),Landscapist 只负责编排请求并把结果画到 Compose。 - 接入
landscapist-glide时最常见的版本坑是什么? 答:它钉死 Glide 4.16.0;项目若用其它 Glide 版本会冲突,需要exclude或对齐 Glide 版本。
速记
- 统一 API:
GlideImage/CoilImage/FrescoImage+ 统一LandscapistImage,imageModel = { url }。 - 两绑定:组合生命周期绑定(离开取消)+ 约束驱动采样(按尺寸降采样)。
- 插件化:placeholder/blur/palette/shimmer 走
ImageComponentBuilder,业务只声明。 - 内存来自引擎:BitmapPool / MemoryCache / Fresco native,Landscapist 只编排。
- KMP:Coil 3 后端原生跨端;
landscapist-glide钉死 Glide 4.16.0。