Android: невозможное исключение NullPointerException при использовании viewModelscope и withContext

Моя проблема в том, что я получаю невозможное NullPointerException. Когда я получаю доступ к своим данным emailEntity из моего price variable без использования elvis-operator, мой price variable становится нулевым, а я получаю NullPointerException.

Теперь возникает проблема: когда я использую elvis-operator в моем price variable и получаю доступ к своим данным 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.<init>(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<EmailEntity?> на 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)

person Andrew    schedule 21.09.2020    source источник
comment
Самое простое решение - прекратить писать код, который может создавать нулевые указатели. emailEntity.value?.let{ } или выполните некоторую проверку на null, назначьте значение по умолчанию, если оно равно null и т. д.   -  person a_local_nobody    schedule 21.09.2020
comment
@a_local_nobody Хорошо, но дело не в этом. Моя проблема в том, что emailEntity не может быть нулевым ни при каких обстоятельствах. И почему одно и то же значение null в одной строке и не null в другой? И когда я использую ваш код, значение всегда является значением по умолчанию, а не ожидаемым значением!   -  person Andrew    schedule 21.09.2020
comment
я понимаю, что вы хотите узнать, почему это происходит, поэтому я написал это как комментарий, а не как ответ :) я просто даю вам очевидное решение вашей проблемы, чтобы вам не пришлось тратить время на это   -  person a_local_nobody    schedule 21.09.2020
comment
@a_local_nobody Я ценю это, но мне приходится тратить на это свое время, потому что это должно быть продуктивным, и поэтому значение должно быть установлено правильно :)   -  person Andrew    schedule 21.09.2020
comment
Эй, попробуйте присвоить значение Mutable Live Data, а затем использовать MutableLiveData(emailEntity.value?.basePrice!!). Вы можете перейти по этой ссылке для получения информации о присвоении значения = stackoverflow.com/questions /51305150/   -  person Mohammed Hanif.    schedule 21.09.2020


Ответы (1)


Ваша сопрограмма выполняет асинхронный код. К тому времени, когда ваш EmailViewModel создан, сопрограмма еще не закончила работу, поэтому в этот момент значение LiveData все еще равно нулю. Вы, должно быть, пытаетесь немедленно получить значение из основного потока, прежде чем сопрограмма завершит работу.

Обычно с LiveData вы почти никогда не извлекаете значение напрямую. Вместо этого вы должны наблюдать за LiveData с обратным вызовом, чтобы вы могли реагировать когда он получает значение, что не произойдет немедленно с функциями приостановки и сопрограммами.

Кстати, вы должны обновлять LiveData только из основного потока, чего вы не можете сделать со своей сопрограммой. Предполагая, что ваши функции приостановки должным образом делегируют фоновые потоки, что имеет место в том случае, если они исходят от вас, использующего библиотеку Room, вы должны удалить обертывающий блок withContext из своей сопрограммы. Правильно составленная функция приостановки всегда может быть безопасно вызвана из Главного диспетчера и при необходимости будет делегирована фоновым диспетчерам.

person Tenfour04    schedule 21.09.2020
comment
Но как я могу наблюдать за своими живыми данными из моей модели представления? Моя электронная почта не нужна вне моей модели представления и поэтому не может наблюдаться. Чего я пытаюсь добиться, так это наблюдать за моим emailRepository из моей модели просмотра и устанавливать значение моей цены. Я не нашел, как это сделать, не могли бы вы добавить это в свой ответ, когда я предоставлю свой электронный репозиторий в своем вопросе? - person Andrew; 21.09.2020
comment
О, в таком случае вам даже не нужны LiveData. Просто поместите свой код для работы с извлеченным значением внутри вашей сопрограммы в конце. - person Tenfour04; 21.09.2020
comment
Да уууу, я понятия не имею, что вы пытаетесь мне сказать и как это сделать :( - person Andrew; 21.09.2020
comment
Есть ли причина, по которой вы используете flow для генерации одного значения? - person Tenfour04; 21.09.2020
comment
В настоящее время я использую flow, но позже я заменю его на callbackFlow, чтобы излучать поток emailEntities из моего firebase cloud firestore, а затем всегда получать обновления. Другая причина использования flow заключается в том, что мне не нужно использовать прослушиватели для получения моих документов. Без использования потока я не могу использовать DocumentReference.get().await(). Или есть более легкая альтернатива? - person Andrew; 21.09.2020
comment
Ваши потоки в том виде, в каком они есть сейчас, не имеют смысла. Эти две функции возвращают Flow объектов, поэтому нет причин, чтобы они были suspend функциями. Фактически, они, вероятно, должны быть val свойствами. Затем ваша исходная сопрограмма в init может получить значение, используя flow.collect(). Я не знаю, что вы переопределяете, поэтому трудно сказать, как это должно выглядеть на самом деле. - person Tenfour04; 21.09.2020
comment
Таким образом, обратный вызов firebase будет вызываться неоднократно, и вы хотите реагировать каждый раз, когда доступно новое значение? - person Tenfour04; 21.09.2020
comment
да, но это может быть другая проблема. Поскольку моя цена до сих пор не назначена, я добавил в сообщение свой текущий метод. - person Andrew; 21.09.2020
comment
Я не совсем понимаю, что вы пытаетесь сделать из фрагментов, которые вы опубликовали, но мне не кажется, что вы должны использовать для этого сопрограммы и потоки. Это ситуация, когда они добавляют ненужную сложность. - person Tenfour04; 21.09.2020
comment
Но КАК я могу получить свой документ из моей firebase? Я не хочу использовать слушателей, они уродливы и абсолютно устарели. Я не понимаю, почему так сложно получить один документ из моей базы данных с помощью сопрограмм kotlin. Чего я пытаюсь добиться, так это: получить документ из базы данных на основе значения объекта. Установите basePrice на мою цену в зависимости от того, какой документ я получил, и работайте с моим emailEntity в моей модели представления, чтобы изменить цены, когда пользователь выбирает что-то в представлении. Я использую привязку данных. - person Andrew; 21.09.2020
comment
Давайте продолжим обсуждение в чате. - person Tenfour04; 21.09.2020