Hilt 注入顺序不一致导致 Compose ViewModel 中出现 NullPointerExceptions

问题描述 投票:0回答:1

我目前正在使用 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
块(在我的情况下触发数据加载)之前完成注入?

请注意,代码是有意简化的,如果可能的话,我想保留这个“加载数据的功能”系统。

android nullpointerexception android-jetpack-compose viewmodel dagger-hilt
1个回答
0
投票

因为 Hilt 似乎还没有完成注射

您的超类正在启动一个协程,该协程取决于子类初始化的完成。这是一个竞争条件:您假设子类的初始化完成将在协程运行之前发生。这是无法保证的,部分原因是您没有采取任何措施来保证这一点。协程独立运行,如果调度程序的变幻莫测导致 IO 协程比您预期的更早运行……您就会遇到这个问题。

要么让子类启动协程(在其构造函数初始化完成后),要么进行某种同步,以便协程将阻塞,直到构造函数初始化完成。

© www.soinside.com 2019 - 2024. All rights reserved.