Android:使用 viewModelscope 和 withContext 时不可能出现 NullPointerException

Posted

技术标签:

【中文标题】Android:使用 viewModelscope 和 withContext 时不可能出现 NullPointerException【英文标题】:Android: Impossible NullPointerException when using viewModelscope and withContext 【发布时间】:2021-01-07 19:09:38 【问题描述】:

我的问题是,我得到了一个不可能的NullPointerException。当我从我的price variable 访问我的emailEntity 数据而不使用elvis-operator 时,我的price variable 为空,我得到一个NullPointerException

现在问题来了:当我在我的 price variable 上使用 elvis-operator 并在函数中访问我的 emailEntity 数据时,我没有得到 NullPointerException 并且价格是正确设置。我做错了什么?

基本代码

class EmailViewModel @ViewModelInject constructor() : ViewModel() 

      // This is the value I access from my price variable and the function
      private val emailEntity = MutableLiveData<EmailEntity?>()

      // Setting the value of my emailEntity here
      init 
          // I have to use viewModelScope because "getCalibratePrice and getRepairPrice" are suspend functions
          viewModelScope.launch 
              withContext(Dispatchers.IO) 
                    when(subject.value.toString()) 
                       "Toast" -> emailEntity.postValue(emailRepository.getCalibratePrice())
                       else -> emailEntity.postValue(emailRepository.getRepairPrice())
                    
              
          
      

问题代码

   // NullPointerException
   val price = MutableLiveData(emailEntity.value?.basePrice!!)

  fun checkIfPriceIsInitialized() 
    Timber.d("Emailprice is $emailEntity.value.basePrice")
  

工作代码

   // NO NullPointerException but value is now always 0F
   val price = MutableLiveData(emailEntity.value?.basePrice ?: 0F)

  // EmailEntity price is correctly set here!!!
  fun checkIfPriceIsInitialized() 
    Timber.d("Emailprice is $emailEntity.value.basePrice")
  

堆栈跟踪

java.lang.NullPointerException
    at com.example.app.framework.ui.viewmodel.EmailViewModel.<init>(EmailViewModel.kt:164)
    at com.example.app.framework.ui.viewmodel.EmailViewModel_AssistedFactory.create(EmailViewModel_AssistedFactory.java:58)
    at com.example.app.framework.ui.viewmodel.EmailViewModel_AssistedFactory.create(EmailViewModel_AssistedFactory.java:20)
    at androidx.hilt.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:76)
    at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:69)
    at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185)
    at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
    at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:54)
    at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
    at com.example.app.framework.ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment.getViewModel(Unknown Source:2)
    at com.example.app.framework.ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment.getViewModel(CalibrateRepairMessageFragment.kt:26)
    at com.example.app.framework.ui.view.basefragments.BaseFragment.onCreateView(BaseFragment.kt:30)
    at com.example.app.framework.ui.view.basefragments.EmailFragment.onCreateView(EmailFragment.kt:54)
    at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2699)
    at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1199)
    at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2236)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2009)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1965)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1861)
    at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

EmailViewModel.&lt;init&gt;(EmailViewModel.kt:164) 指向 -> val price = MutableLiveData(emailEntity.value?.basePrice!!)

请记住,我是从零开始使用 kotlin 协程的。因此,我不知道 100% 到底是如何工作的

编辑

这是我的仓库:

interface EmailRepository 
    fun sendEmail(email: Email): Flow<EmailStatus<Unit>>
    suspend fun getCalibratePrice(): Flow<EmailEntity?>
    suspend fun getRepairPrice(): Flow<EmailEntity?>

这是我的实现:

class EmailRepositoryImpl @Inject constructor(
    private val db: FirebaseFirestore
) : EmailRepository 

override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow 
        val result = db.collection("emailprice").document("Kalibrieren").get().await()
        val emailEntity = result.toObject<EmailEntity?>()
        emit(emailEntity)
    .catch 
        Timber.d("Error on getCalibrate Price")
    .flowOn(Dispatchers.Main)

    override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow 
        val collection = db.collection("emailprice").document("Reparieren").get().await()
        val emailEntity = collection.toObject<EmailEntity?>()
        emit(emailEntity)
    .catch 
        Timber.d("Error on getRepairPrice")
    .flowOn(Dispatchers.Main)


替代方法是在末尾使用.single(),并将返回类型从Flow&lt;EmailEntity?&gt; 更改为EmailEntity

编辑 2

private var emailEntity: EmailEntity = EmailEntity("", 50F)

init 
    viewModelScope.launch 
        when(subject.value.toString()) 
            context.getString(R.string.home_calibrate_card_headline) -> emailRepository.getCalibratePrice().collect 
                emailEntity = it ?: EmailEntity("Error", 100F)
            
            else -> emailRepository.getRepairPrice().collect 
                emailEntity = it ?: EmailEntity("Error Zwei", 150F)
            
        
    


// Price is 50 and does not change..
val price = MutableLiveData(emailEntity.basePrice)

【问题讨论】:

最简单的解决方案是停止编写可能产生空指针的代码。 emailEntity.value?.let 或者做一些空值检查,如果它是 null 则分配一个默认值,等等 @a_local_nobody 好吧,但这不是重点。我的问题是, emailEntity 在任何情况下都不能为空。为什么同一件事在一行为空,而在另一行不为空?而且当我使用您的代码时,该值始终是默认值,而不是预期值! 我了解您想了解它发生的原因,这就是为什么我将其写为评论而不是答案:) 我只是为您提供了一个明显的解决方案,所以您不要不必为此浪费时间 @a_local_nobody 我很欣赏这一点,但我不得不在这上面浪费时间,因为这必须高效,因此应该正确设置值:) 嘿,尝试为您的Mutable Live Data 分配一些值,然后使用MutableLiveData(emailEntity.value?.basePrice!!)。您可以点击此链接获取分配值的参考=***.com/questions/51305150/… 【参考方案1】:

您的协程正在运行异步代码。当您的EmailViewModel 被实例化时,协程还没有完成运行,所以此时LiveData 的值仍然为空。在协程完成运行之前,您必须尝试立即从主线程中检索该值。

通常使用 LiveData,您几乎从不直接检索值。相反,您应该使用回调观察 LiveData,以便您可以在它获得值时做出反应,而挂起函数和协程不会立即发生这种情况。

顺便说一句,您应该只从主线程更新 LiveData,而您的协程无法做到这一点。假设您的挂起函数正确委托给后台线程,如果它们来自您使用 Room 库,您应该从协程中删除包装 withContext 块。始终可以从 Main Dispatcher 安全地调用正确组合的挂起函数,并在内部根据需要委托给后台调度程序。

【讨论】:

但是我怎样才能从我的视图模型中观察我的实时数据呢?在我的视图模型之外不需要我的 emailEntity,因此无法观察到。我想要实现的是从我的视图模型中“观察”我的 emailRepository 并设置我的价格值。我还没有找到如何做到这一点,当我在我的问题中提供我的 emailRepository 时,您能否将其添加到您的答案中? 哦,在这种情况下,您甚至不需要 LiveData。只需将用于处理检索到的值的代码放在最后的协程中即可。 是的 uuuh,我不知道你想告诉我什么以及如何做 :( 您使用flow 发出单个值是否有原因? 目前,我使用的是flow,但稍后我将使用callbackFlow 更改它,以便从我的firebase cloud firestore 发出emailEntities 流,然后始终获取更新。使用flow 的另一个原因是我不必使用侦听器来获取我的文档。如果不使用流量,我将无法使用DocumentReference.get().await()。还是有更简单的选择?

以上是关于Android:使用 viewModelscope 和 withContext 时不可能出现 NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章

协程ViewModelScope源码解析

关于协程,详解ViewModelScope

Kotlin Coroutines viewModelScope 中的改造调用

Kotlin之协程coroutine lifecycleScope 和 viewModelScope源码

lifecycleScope 和viewModelScope

Kotlin 协程协程底层实现 ④ ( 结构化并发 | viewModelScope 作用域示例 )