4

My problem is, that when I try to get a document out of my database, that this document aka the object is always null. I only have this problem when I use Kotlin Coroutines to get the document out of my database. Using the standard approach with listeners do work.

EmailRepository

interface EmailRepository {
    suspend fun getCalibratePrice(): Flow<EmailEntity?>
    suspend fun getRepairPrice(): Flow<EmailEntity?>
}

EmailRepository Implementation

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

    fun hasInternet(): Boolean {
        return true
    }

    // This works! When using flow to write a document, the document is written!
    override fun sendEmail(email: Email)= flow {
        emit(EmailStatus.loading())
        if (hasInternet()) {
            db.collection("emails").add(email).await()
            emit(EmailStatus.success(Unit))
        } else {
            emit(EmailStatus.failed<Unit>("No Email connection"))
        }
    }.catch {
        emit(EmailStatus.failed(it.message.toString()))
    }.flowOn(Dispatchers.Main)


    // This does not work! "EmailEntity" is always null. I checked the document path!
    override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
        val result = db.collection("emailprice").document("Kalibrieren").get().await()
        emit(result.toObject<EmailEntity>())
    }.catch {

    }.flowOn(Dispatchers.Main)


    // This does not work! "EmailEntity" is always null. I checked the document path!
    override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
        val result = db.collection("emailprice").document("Reparieren").get().await()
        emit(result.toObject<EmailEntity>())
    }.catch {

    }.flowOn(Dispatchers.Main)
}

Viewmodel where I get the data

init {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                if (subject.value != null){
                    when(subject.value) {
                        "Test" -> {
                            emailRepository.getCalibratePrice().collect {
                                emailEntity.value = it
                            }
                        }
                        "Toast" -> {
                            emailRepository.getRepairPrice().collect {
                                emailEntity.value = it
                            }
                        }
                    }
                }
            }
        }
    }

private val emailEntity = MutableLiveData<EmailEntity?>()

private val _subject = MutableLiveData<String>()
val subject: LiveData<String> get() = _subject

Fragment

@AndroidEntryPoint
class CalibrateRepairMessageFragment() : EmailFragment<FragmentCalibrateRepairMessageBinding>(
    R.layout.fragment_calibrate_repair_message,
) {
    // Get current toolbar Title and send it to the next fragment.
    private val toolbarText: CharSequence by lazy { toolbar_title.text }

    override val viewModel: EmailViewModel by navGraphViewModels(R.id.nav_send_email) { defaultViewModelProviderFactory }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Here I set the data from the MutableLiveData "subject". I don't know how to do it better
        viewModel.setSubject(toolbarText.toString())
    }
}

One would say, that the Firebase rules are the problems here, but that should not be the case here, because the database is open and using the listener approach does work.

I get the subject.value from my CalibrateRepairMessageFragment. When I don't check if(subject.value != null) I get a NullPointerException from my init block.

I will use the emailEntitiy only in my viewModel and not outside it.

I appreciate every help, thank you.

EDIT

This is the new way I get the data. The object is still null! I've also added Timber.d messages in my suspend functions which also never get executed therefore flow never throws an error.. With this new approach I don't get a NullPointerException anymore

private val emailEntity = liveData {
    when(subject.value) {
        "Test" -> emailRepository.getCalibratePrice().collect {
            emit(it)
        }
        "Toast" -> emailRepository.getRepairPrice().collect {
            emit(it)
        }
        // Else block is never executed, therefore "subject.value" is either Test or toast and the logic works. Still error when using flow!
        else -> EmailEntity("ERROR", 0F)
    }
}

I check if the emailEntity is null or not with Timber.d("EmailEntity is ${emailEntity.value}") in one of my functions.

I then set the price with val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) but because emailentity is null the price is always 1000

EDIT 2

I have now further researched the problem and made a big step forward. When observing the emailEntity from a fragment like CalibrateRepairMessageFragment the value is no longer null.

Furthermore, when observing emailEntity the value is also not null in viewModel, but only when it is observed in one fragment! So how can I observe emailEntity from my viewModel or get the value from my repository and use it in my viewmodel?

Andrew
  • 4,264
  • 1
  • 21
  • 65

1 Answers1

3

Okay, I have solved my problem, this is the final solution:

Status class

sealed class Status<out T> {
    data class Success<out T>(val data: T) : Status<T>()
    class Loading<T> : Status<T>()
    data class Failure<out T>(val message: String?) : Status<T>()

    companion object {
        fun <T> success(data: T) = Success<T>(data)
        fun <T> loading() = Loading<T>()
        fun <T> failed(message: String?) = Failure<T>(message)
    }
}

EmailRepository

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

EmailRepositoryImpl

class EmailRepositoryImpl (private val db: FirebaseFirestore) : EmailRepository {
    fun hasInternet(): Boolean {
        return true
    }

    override fun sendEmail(email: Email)= flow {
        Timber.d("Executed Send Email Repository")
        emit(Status.loading())
        if (hasInternet()) {
            db.collection("emails").add(email).await()
            emit(Status.success(Unit))
        } else {
            emit(Status.failed<Unit>("No Internet connection"))
        }
    }.catch {
        emit(Status.failed(it.message.toString()))
    }.flowOn(Dispatchers.Main)

    // Sends status and object to viewModel
    override suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
        emit(Status.loading())
        val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
        emit(Status.success(entity))
    }.catch {
        Timber.d("Error on getCalibrate Price")
        emit(Status.failed(it.message.toString()))
    }

    // Sends status and object to viewModel
    override suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
        emit(Status.loading())
        val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
        emit(Status.success(entity))
    }.catch {
        Timber.d("Error on getRepairPrice")
        emit(Status.failed(it.message.toString()))
    }
}

ViewModel

private lateinit var calibrateRepairPrice: CalibrateRepairPricing

private val _calirateRepairPriceErrorState = MutableLiveData<Status<Unit>>()
val calibrateRepairPriceErrorState: LiveData<Status<Unit>> get() = _calirateRepairPriceErrorState

init {
        viewModelScope.launch {
            when(_subject.value.toString()) {
                "Toast" -> emailRepository.getCalibratePrice().collect {
                    when(it) {
                        is Status.Success -> {
                            calibrateRepairPrice = it.data!!
                            _calirateRepairPriceErrorState.postValue(Status.success(Unit))
                        }
                        is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
                        is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
                    }
                }
                else -> emailRepository.getRepairPrice().collect {
                    when(it) {
                        is Status.Success -> {
                            calibrateRepairPrice = it.data!!
                            _calirateRepairPriceErrorState.postValue(Status.success(Unit))
                        }
                        is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
                        is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
                    }
                }
            }
            price.postValue(calibrateRepairPrice.head!!.basePrice)
        }
    }

You can now observe the status in one of your fragments (but you dont need to!)

Fragment

viewModel.calibrateRepairPriceErrorState.observe(viewLifecycleOwner) { status ->
            when(status) {
                is Status.Success -> requireContext().toast("Price successfully loaded")
                is Status.Loading -> requireContext().toast("Price is loading")
                is Status.Failure -> requireContext().toast("Error, Price could not be loaded")
            }
        }

This is my toast extensions function:

fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, text, duration).show()
}
Andrew
  • 4,264
  • 1
  • 21
  • 65