UI не обновляется при изменении БД

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

Идея состоит в том, что приложение запускается с пустыми таблицами и переходит к чтению из базы данных Firestore для получения данных, сохраняет данные в локальной базе данных SqlLite (используя Room) и отображает обновленные данные. Всякий раз, когда данные обновляются в Firestore, они должны обновляться в SqlLite и обновлять пользовательский интерфейс.

Однако мой пользовательский интерфейс (пока только текстовое поле) обновляется только при запуске приложения и никогда после изменения БД.

ПортероДао

package com.sarcobjects.portero.db

import androidx.room.*
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits

@Dao
abstract class PorteroDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract suspend fun insert(portero: Portero): Long

    @Transaction
    @Query("SELECT * FROM Portero WHERE porteroId == :porteroId")
    abstract suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits
}

Portero Репозиторий

package com.sarcobjects.portero.repository

import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.sarcobjects.portero.db.PorteroDao
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import timber.log.Timber.d
import timber.log.Timber.w
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PorteroRepository @Inject constructor(
    private val porteroDao: PorteroDao,
    private val firestore: FirebaseFirestore
) {

    @ExperimentalCoroutinesApi
    suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits {
        GlobalScope.launch {refreshPortero(porteroId)}
        val portero = porteroDao.getPortero(porteroId)
        d("Retrieved portero: $portero")
        return portero
    }

    @ExperimentalCoroutinesApi
    private suspend fun refreshPortero(porteroId: Long) {
        d("Refreshing")
        //retrieve from firestore
        retrieveFromFirestore(porteroId)
            .collect { portero ->
                d("Retrieved and collected: $portero")
                porteroDao.insert(portero)
            }
    }

    @ExperimentalCoroutinesApi
    private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
        val callback = EventListener<DocumentSnapshot> { document, e ->
            if (e != null) {
                w(e, "Listen from Firestore failed.")
                close(e)
            }
            d("Read successfully from Firestore")
            if (document != null && document.exists()) {
                //Convert to objects
                val portero = document.toObject(Portero::class.java)
                d("New Portero: ${portero.toString()}")
                offer(portero!!)
            } else {
                d("Portero not found for porteroId: $porteroId")
            }
        }
        val addSnapshotListener = firestore.collection("portero").document(porteroId.toString())
            .addSnapshotListener(callback)
        awaitClose { addSnapshotListener.remove()}
    }
}

Кнопки ViewModel

package com.sarcobjects.portero.ui.buttons

import androidx.hilt.Assisted
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import com.sarcobjects.portero.repository.PorteroRepository
import timber.log.Timber.d


class ButtonsViewModel @ViewModelInject
constructor(@Assisted savedStateHandle: SavedStateHandle, porteroRepository: PorteroRepository) : ViewModel() {

    private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
    val portero: LiveData<PorteroWithLevelsAndUnits> = liveData {
        val data = porteroRepository.getPortero(porteroId)
        d("Creating LiveData with: $data")
        emit(data)
    }
}

КнопкиФрагмент

package com.sarcobjects.portero.ui.buttons

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.sarcobjects.portero.R
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.buttons_fragment.*
import timber.log.Timber.d

@AndroidEntryPoint
class ButtonsFragment : Fragment() {

    companion object {
        fun newInstance() = ButtonsFragment()
    }

    private val viewModel: ButtonsViewModel by viewModels (
    )

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return inflater.inflate(R.layout.buttons_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.portero.observe(viewLifecycleOwner, Observer<PorteroWithLevelsAndUnits> {porteroWLAU ->
            d("Observing portero: $porteroWLAU")
            message.text = porteroWLAU?.portero?.name ?: "Portero not found."
        })

    }
}

Кажется, что с внедрением зависимостей все в порядке (без NPE), я даже проверил, что экземпляр ViewModel одинаков во фрагменте и в самой ViewModel, а сохранение через Room правильное; новые данные фактически сохраняются в SqlLite, когда я обновляю Firestore. Кроме того, никаких исключений или ошибок в logcat. Но UI не обновляется.

1 ответ

Решение

Итак, мне удалось найти способ заставить эту работу работать, хотя и по-другому. Моя идея заключалась в том, чтобы заставить Room запускать перезагрузку liveData всякий раз, когда я писал в SqlLite, но мне так и не удалось заставить его работать, и до сих пор я не знаю почему.

В итоге я сделал следующее:

Вернуть поток из репозитория, инициированный обновлениями в Firestore:

    @ExperimentalCoroutinesApi
    fun getPorteroFlow(porteroId: Long): Flow<Portero> = retrieveFromFirestore(porteroId)

    @ExperimentalCoroutinesApi
    private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
        val callback = EventListener<DocumentSnapshot> { document, e ->
            if (e != null) {
                w(e, "Listen from Firestore failed.")
                return@EventListener
            }
            d("Read successfully from Firestore")
            if (document != null && document.exists()) {
                //Convert to objects
                val portero = document.toObject(Portero::class.java)
                d("New Portero: ${portero.toString()}")
                GlobalScope.launch {
                    d("Saved new portero: $portero")
                    porteroDao.insert(portero!!)
                }
                offer(portero!!)
            } else {
                d("Portero not found for porteroId: $porteroId")
            }
        }
        val addSnapshotListener = firestore.collection("portero").document(porteroId.toString()) //.get()
            .addSnapshotListener(callback)
        awaitClose { addSnapshotListener.remove()}
    }

Преобразуйте поток в liveData в ViewModel:

    private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
    @ExperimentalCoroutinesApi
    val portero = porteroRepository.getPorteroFlow(porteroId)
        .onStart { porteroRepository.getPortero(porteroId) }
        .asLiveData()
}

(onStart используется для чтения данных из SqlLite при запуске приложения, если нет интернета и Firestore недоступен).

Это работает безупречно и очень быстро. Как только я обновляю данные в консоли Firestore, я вижу обновление пользовательского интерфейса на устройстве.

Другие вопросы по тегам