Compose+Hilt项目ViewModel注入顺序异常引发空指针问题求助
问题核心
你的场景中,父类ArrowWordsViewModel的init块会直接调用computeViewModelInternally触发数据加载,但Hilt注入子类AdsViewModel的依赖时,偶发出现父类初始化逻辑先于子类属性完成初始化的情况——具体表现为子类的属性初始化日志(CPT ERROR ATTRIBUTE)在computeViewModel之后输出,导致子类重写的computeViewModel方法访问未初始化的savedStateHandle抛出NPE。
这本质是Kotlin类初始化顺序的特性:父类的init块会在子类构造参数初始化后、子类init块执行前触发,如果子类重写了父类调用的方法,此时子类自身的属性(尤其是在子类init块中初始化的属性)可能还未完成赋值。
解决方案(保留现有数据加载机制)
1. 调整父类初始化逻辑的触发时机
移除父类init块中的自动触发逻辑,改为由子类在完全初始化后手动调用:
父类ArrowWordsViewModel修改:
open class ArrowWordsViewModel { // 移除原来的init块,将数据加载逻辑封装为可调用方法 protected fun initialize() { computeViewModelInternally() } private fun computeViewModelInternally() { // 原父类init块中的前置逻辑 computeViewModel() } protected open fun computeViewModel() { // 父类基础数据加载逻辑 } }
子类AdsViewModel修改:
@HiltViewModel class AdsViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, // 其他Hilt注入的依赖 ) : ArrowWordsViewModel() { // 子类自身的属性初始化(如果有) private val adConfig = loadAdConfig() init { // 子类所有属性(包括Hilt注入的依赖)初始化完成后,触发数据加载 initialize() } override fun computeViewModel() { // 此时savedStateHandle已完全初始化,可安全访问 val adId = savedStateHandle.get<String>("selected_ad_id") // 子类数据加载业务逻辑 } }
2. 优化子类属性初始化方式
如果子类存在将构造注入依赖赋值给lateinit属性的情况,直接改为在构造参数中声明为类属性,避免中间赋值环节导致的初始化延迟:
// 错误示例(可能导致NPE) class AdsViewModel @Inject constructor( savedStateHandle: SavedStateHandle ) : ArrowWordsViewModel() { private lateinit var mySavedStateHandle: SavedStateHandle init { mySavedStateHandle = savedStateHandle } } // 正确示例 class AdsViewModel @Inject constructor( private val mySavedStateHandle: SavedStateHandle ) : ArrowWordsViewModel() { // 无需额外赋值,直接使用 }
3. 测试环境同步验证
确保测试中使用的Fake依赖提供者是同步完成注入的,避免异步逻辑导致的初始化顺序混乱——比如检查CPT ERROR - PROVIDE FAKE对应的Fake实现,确保依赖注入是同步返回的,没有延迟。
效果验证
修改后,日志顺序会稳定为:CPT ERROR - PROVIDE FAKE → CPT ERROR Parent constructor → CPT ERROR INIT → CPT ERROR ATTRIBUTE → CPT ERROR computeViewModel parent → CPT ERROR computeViewModel
确保子类属性完全初始化后才执行数据加载逻辑,彻底避免偶发NPE。
内容的提问来源于stack exchange,提问作者rolandl

