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, я вижу обновление пользовательского интерфейса на устройстве.