我目前正在使用 Compose 和 Hilt 开发一个应用程序,偶尔(根据我的自动化测试,1000 次中不到 10 次),对 ViewModel 的调用顺序会有所不同,从而导致 NullPointerExceptions,因为我正在操作以下属性希尔特显然还没有注射。
我的 ViewModel 继承自基本 ViewModel:
abstract class ArrowWordsViewModel(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO.also { println("CPT ERROR Parent constructor") }
) :
ViewModel() {
init {
println("CPT ERROR INIT")
computeViewModelInternally(true)
}
open suspend fun computeViewModel() {}
private fun computeViewModelInternally() {
Timber.d("CPT ERROR - computeViewModelInternal")
viewModelScope.launch(dispatcher + CoroutineExceptionHandler { _, throwable ->
viewModelScope.launch(Dispatchers.Main) {
println("CPT ERROR THROWABLE- ${throwable.javaClass.name}")
}
}) {
println("CPT ERROR computeViewModel parent")
computeViewModel()
}
}
}
我有一个继承自它的 ViewModel。例如:
@HiltViewModel
class AdsViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val preferences: ArrowWordsPreferences,
private val fake: Int
) : ArrowWordsViewModel() {
val isOnBoardingContextTest= savedStateHandle.get<Boolean>(ScreenArgs.ADS_IS_ONBOARDING_EXTRA).also { println("CPT ERROR ATTRIBUTE") }
val isOnBoardingContext = MutableStateFlow(false)
val showAlertDialog = MutableStateFlow(false)
override suspend fun computeViewModel() {
println("CPT ERROR computeViewModel")
super.computeViewModel()
isOnBoardingContext.value = savedStateHandle.get<Boolean>(ScreenArgs.ADS_IS_ONBOARDING_EXTRA) ?: false
}
}
注入的元素在 Hilt 模块中定义:
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun providePreferences(@ApplicationContext context: Context): ArrowWordsPreferences =
ArrowWordsPreferences(context)
@Provides
fun provideFake() : Int =
1.also { println("CPT ERROR - PROVIDE FAKE") }
}
我的 ViewModel 然后在可组合项中使用:
@Composable
fun AdsScreen(
modifier: Modifier = Modifier,
viewModel: AdsViewModel = hiltViewModel(),
onBackClicked: () -> Unit,
onOnBoardingFinished: () -> Unit
) {
//...
}
在 99% 的情况下,一切正常。但有时,我会在
NullPointerException
类的 isOnBoardingContext.value = savedStateHandle.get<Boolean>(ScreenArgs.ADS_IS_ONBOARDING_EXTRA) ?: false
方法中调用 computeViewModel
线上得到一个 AdsViewModel
。
如您所见,我添加了日志和假注入来尝试理解我的代码流程。
在 99% 的情况下,当一切顺利时,日志会产生以下输出:
CPT ERROR - PROVIDE FAKE
CPT ERROR Parent constructor
CPT ERROR INIT
CPT ERROR ATTRIBUTE
CPT ERROR computeViewModel parent
CPT ERROR computeViewModel
但是当我得到
NullPointerException
时,会显示以下日志:
CPT ERROR - PROVIDE FAKE
CPT ERROR Parent constructor
CPT ERROR INIT
CPT ERROR computeViewModel parent
CPT ERROR computeViewModel
CPT ERROR ATTRIBUTE
CPT ERROR THROWABLE- java.lang.NullPointerException
我们可以看到差异在于
CPT ERROR ATTRIBUTE
日志的级别,它不在同一个地方。
根据 Kotlin 文档:
在实例初始化期间,初始化程序块按照它们在类主体中出现的顺序执行,与属性初始化程序交错:
因此,我得出的结论是,在我的例子中,
ArrowWordsViewModel
类的构造函数在 Hilt 注入 AdsViewModel
类的构造函数结束之前被调用。由于调用了 ArrowWordsViewModel
类的主构造函数,因此它允许调用 init
块,该块会触发 computeViewModel
方法并导致 NullPointerExceptions
,因为 Hilt 似乎尚未完成其注入(因为之后调用 AdsViewModel
类的属性)。
如何修改我的代码以确保 Hilt 在调用父类的
init
块(在我的情况下触发数据加载)之前完成注入?
请注意,代码是有意简化的,如果可能的话,我想保留这个“加载数据的功能”系统。
因为 Hilt 似乎还没有完成注射
您的超类正在启动一个协程,该协程取决于子类初始化的完成。这是一个竞争条件:您假设子类的初始化完成将在协程运行之前发生。这是无法保证的,部分原因是您没有采取任何措施来保证这一点。协程独立运行,如果调度程序的变幻莫测导致 IO 协程比您预期的更早运行……您就会遇到这个问题。
要么让子类启动协程(在其构造函数初始化完成后),要么进行某种同步,以便协程将阻塞,直到构造函数初始化完成。