Android: объект Firebase равен нулю при использовании потока kotlin

#android #kotlin #google-cloud-firestore #android-livedata #android-mvvm

#Android #kotlin #google-облако-firestore #android-livedata #android-mvvm

Вопрос:

Моя проблема в том, что когда я пытаюсь получить документ из своей базы данных, этот документ, он же объект, всегда равен нулю. У меня возникает эта проблема только тогда, когда я использую сопрограммы Kotlin для извлечения документа из моей базы данных. Использование стандартного подхода со слушателями действительно работает.

Репозиторий электронной почты

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

Реализация EmailRepository

 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, где я получаю данные

 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
  

Фрагмент

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

Можно было бы сказать, что проблемы здесь связаны с правилами Firebase, но здесь этого не должно быть, потому что база данных открыта, и использование подхода слушателя действительно работает.

Я получаю subject.value от моего CalibrateRepairMessageFragment . Когда я не проверяю if(subject.value != null) , я получаю исключение NullPointerException из моего init блока.

Я буду использовать emailEntitiy только в моей ViewModel, а не за ее пределами.

Я ценю любую помощь, спасибо.

Редактировать

Это новый способ получения данных. Объект по-прежнему равен нулю! Я также добавил Timber.d сообщения в свои функции приостановки, которые также никогда не выполняются, поэтому поток никогда не выдает ошибку.. Благодаря этому новому подходу я больше не получаю исключение NullPointerException

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

Я проверяю, имеет ли значение emailEntity значение null или нет, Timber.d("EmailEntity is ${emailEntity.value}") в одной из моих функций.

Затем я устанавливаю цену с val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) помощью but, потому emailentity null что цена всегда равна 1000

РЕДАКТИРОВАТЬ 2

Теперь я дополнительно изучил проблему и сделал большой шаг вперед. При наблюдении emailEntity из фрагмента, подобного CalibrateRepairMessageFragment значению, больше нет null .

Кроме того, при наблюдении emailEntity значение также отсутствует null viewModel , но только тогда, когда оно наблюдается в одном фрагменте! Итак, как я могу наблюдать emailEntity из my viewModel или получить значение из my repository и использовать его в my viewmodel ?

Ответ №1:

Хорошо, я решил свою проблему, это окончательное решение:

Класс состояния

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

Репозиторий электронной почты

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

Теперь вы можете наблюдать статус в одном из ваших фрагментов (но вам это не нужно!)

Фрагмент

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

Это моя функция расширений toast:

 fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, text, duration).show()
}