MVVM架构下如何在ViewModel中获取并设置Drawable资源
ViewModel 设计上禁止持有 Fragment、Activity、View 等短生命周期对象的引用,直接传入这类 Context 会引发内存泄漏,同时 Drawable 本身属于纯 UI 层资源,本身就不应该耦合到 ViewModel 层,以下是按推荐优先级排序的可行方案:
这是最符合MVVM分层职责的做法,零内存泄漏风险。ViewModel 只负责返回资源ID这类纯数据,完全不接触Drawable、Context相关的UI逻辑,资源加载、视图设置的工作全部交还给对应生命周期的Fragment/Activity处理。
代码示例:
// ViewModel 中仅持有资源ID,不做资源加载逻辑 val defaultWaypointIconRes = R.drawable.waypoints_sq_blank
// Fragment 中获取数据后自行加载Drawable,设置给marker marker.icon = ContextCompat.getDrawable(requireContext(), viewModel.defaultWaypointIconRes)
如果需要根据业务逻辑动态切换图标,也只需要在ViewModel中判断后返回对应的资源ID即可,不需要生成Drawable实例。
如果确实有场景需要在ViewModel层拿到Drawable,可以继承Jetpack自带的AndroidViewModel类,这个类自带Application级别的全局Context,生命周期和应用进程一致,不会引发内存泄漏。
注意:绝对不要在普通ViewModel中通过构造参数传入Activity/Fragment/View类型的Context,仅可使用AndroidViewModel提供的Application Context。
代码示例:
// 继承AndroidViewModel,构造函数接收Application对象 class MapViewModel(application: Application) : AndroidViewModel(application) { fun getDefaultWaypointIcon(): Drawable? { // 通过getApplication()获取全局Context加载资源 return ContextCompat.getDrawable(getApplication(), R.drawable.waypoints_sq_blank) } }
额外提醒:即使使用这个方案,也不建议在ViewModel中长期缓存Drawable实例,避免Drawable间接关联View引用引发泄漏,最好按需加载后直接传递给UI层使用。
如果项目用了依赖注入框架,可以把资源获取能力抽象成接口,ViewModel仅依赖抽象接口不直接接触Context,既解耦,写单元测试时也可以很方便地mock依赖,不需要带Android运行环境。
代码示例:
- 先定义资源提供的通用接口
interface ResourceProvider { fun getDrawable(@DrawableRes resId: Int): Drawable? // 可以按需扩展getString、getColor等其他资源获取方法 }
- 写接口实现类,内部用全局Application Context加载资源
class ResourceProviderImpl(private val appContext: Context) : ResourceProvider { override fun getDrawable(resId: Int): Drawable? { return ContextCompat.getDrawable(appContext, resId) } }
- 通过DI框架(Hilt/Dagger等)把ResourceProvider的实例注入到ViewModel中,调用时直接用接口方法获取Drawable即可,不需要持有任何Context引用。
- 不要为了图方便在ViewModel中持有Activity/Fragment/View的引用,配置变更(如转屏)、页面销毁时这类短生命周期对象会被回收,ViewModel持有引用会直接导致内存泄漏,甚至出现空指针崩溃。
- Drawable属于UI渲染细节,除非必要不要下沉到ViewModel层,ViewModel的核心职责是管理业务数据和状态,UI渲染相关的逻辑尽量留在UI层处理。
内容的提问来源于stack exchange,提问作者user19109604

