2

My problem is, that I get an impossible NullPointerException. When I access my emailEntity data from my price variable without using an elvis-operator, my price variable gets null and I get a NullPointerException.

Now comes the problem: When I use an elvis-operator at my price variable and access my emailEntity data within a function, I do not get a NullPointerException and the price is correctly set. What am I doing wrong?

Base Code

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())
                    }
              }
          }
      }
}

Problem Code

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

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

Working Code

   // 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}")
  }

StackTrace

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) points to -> val price = MutableLiveData(emailEntity.value?.basePrice!!)

Please bear in mind that I started with kotlin coroutines from scratch. Therefore I do not know 100% how it all really works

EDIT

This is my repository:

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

And this is my implementation:

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)

}

The alternative would be to use .single() at the end and change the return type from Flow<EmailEntity?> to EmailEntity

EDIT 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)
Andrew
  • 4,264
  • 1
  • 21
  • 65
  • simplest solution is to stop writing code which can produce nullpointers. `emailEntity.value?.let{ }` or do some null checking, assign a default value if it is null , etc – a_local_nobody Sep 21 '20 at 16:53
  • @a_local_nobody Well but that is not the point here. My problem is, that emailEntity can't be null under any circumstance. And why is the same thing null at one line and not null at another? And when I use your code, the value is always the default value and never the expected value! – Andrew Sep 21 '20 at 16:54
  • i understand that you want to learn why it happens, that's why i wrote it as a comment and not as an answer :) i'm just giving you an obvious solution to your problem so you don't have to waste time on this – a_local_nobody Sep 21 '20 at 16:56
  • @a_local_nobody I appreciate that, but I have to waste my time on this, because this has to go productive and therefore the value should be set correctly :) – Andrew Sep 21 '20 at 16:57
  • Hey, try assigning some value to your `Mutable Live Data` and then use `MutableLiveData(emailEntity.value?.basePrice!!)`. You can follow this link for refrence of assigning value = https://stackoverflow.com/questions/51305150/mutablelivedata-with-initial-value – WhiteSpidy. Sep 21 '20 at 17:20

1 Answers1

1

Your coroutine is running asynchronous code. By the time your EmailViewModel is instantiated, the coroutine hasn't finished running yet, so at that point, the value of the LiveData is still null. You must be trying to retrieve the value immediately from your main thread, before the coroutine finishes running.

Typically with a LiveData, you almost never retrieve a value directly. Instead, you should observe the LiveData with a callback so you can react when it gets a value, which is not going to happen immediately with suspend functions and coroutines.

By the way, you should only update LiveData from the main thread, which you are failing to do with your coroutine. Assuming your suspend functions properly delegate to background threads, which is the case if they're from you using the Room library, you should remove the wrapping withContext block from your coroutine. A properly composed suspend function can always be called from the Main Dispatcher safely, and will delegate to background dispatchers as necessary internally.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • 1
    But how can I observer my livedata from my viewmodel? My emailEntity is not needed outside my viewmodel and therefore cannot be observed. What I am trying to achieve is to "observe" my emailRepository from my viewmodel and set the value of my price. I have not found how to do this, could you maybe add this to your answer when I provide my emailRepository in my question? – Andrew Sep 21 '20 at 17:40
  • 1
    Oh in that case, you don't even need LiveData. Just put your code for dealing with the retrieved value inside your coroutine at the end. – Tenfour04 Sep 21 '20 at 17:41
  • Yeah uuuh, I have zero clue what you are trying to tell me and how to do tha :( – Andrew Sep 21 '20 at 17:46
  • Is there a reason you're using `flow` to emit a single value? – Tenfour04 Sep 21 '20 at 17:48
  • Currently, I am using `flow`, but I will later change it with `callbackFlow` to emit a stream of `emailEntities` from my `firebase cloud firestore` and then always get the updates. Another reason of using `flow` is that I don't have to use listeners to get my documents. Without using flow I cannot use `DocumentReference.get().await()`. Or is there an easier alternative? – Andrew Sep 21 '20 at 17:51
  • Your flows as they are now don't make sense. These two functions return `Flow` objects, so there's no reason for them to be `suspend` functions. In fact, they should probably be `val` properties. Then your original coroutine in `init` can retrieve a value using `flow.collect()`. I don't know what you're overriding though, so it's hard to say how this should actually look. – Tenfour04 Sep 21 '20 at 17:56
  • So the firebase callback will repeatedly be called and you want to react every time a new value is available? – Tenfour04 Sep 21 '20 at 18:14
  • yes, but that may be another problem. Because my "price" is still not assigned, I've added my current method to the post – Andrew Sep 21 '20 at 18:21
  • I don't totally understand what you're trying to do from the bits and pieces you posted, but it doesn't look to me like you should be using coroutines and flows for it. This is a situation where they are adding unnecessary complexity. – Tenfour04 Sep 21 '20 at 18:32
  • But HOW am I able to get my document out of my firebase? I don't want to use listeners, they are ugly and absolutely outdated. I don't understand why it is so hard to get one document out of my database using kotlin coroutines.. What I am trying to achieve is: Get the document out of the database based on what value "subject" is. Set the basePrice to my price based on what document I got and work with my emailEntity within my viewModel, to change the prices when the user selects something in the view. I am using databinding. – Andrew Sep 21 '20 at 18:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/221834/discussion-between-tenfour04-and-andrew). – Tenfour04 Sep 21 '20 at 18:50