问题描述
我正在使用Android体系结构组件编写应用程序,该应用程序最初是基于著名的文章,但是现在已经过时且不准确,因此基于其他文档,文章和视频,我使用最新的组件来构建一些东西。结果是一个非常简单的架构,只需要很少的代码。
想法是该应用程序从其表为空开始,然后从Firestore数据库读取以获取其数据,将数据存储在本地SqlLite DB中(使用Room)并显示更新的数据。每当在Firestore上更新数据时,都应该在SqlLite中更新数据并更新UI。
但是,我的UI(现在仅是一个文本框)仅在应用程序启动时更新,而在修改数据库之后也不会更新。
PorteroDao
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
}
PorteroRepository
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()}
}
}
ButtonsViewModel
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)
}
}
ButtonsFragment
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),我什至检查了Fragment和ViewModel本身的ViewModel实例是否相同,并且通过Room的持久性是正确的。当我更新Firestore时,新数据实际上已保存到SqlLite中。另外,logcat中没有异常或错误。 但是用户界面未更新。
解决方法
因此,尽管设法不同,我设法找到了一种使这项工作有效的方法。我的想法是每当我写SqlLite时,就让Room触发liveData重新加载,但我从未设法使其生效,但我仍然不知道为什么。
我最后所做的是:
由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()}
}
在ViewModel中将流转换为liveData:
private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
@ExperimentalCoroutinesApi
val portero = porteroRepository.getPorteroFlow(porteroId)
.onStart { porteroRepository.getPortero(porteroId) }
.asLiveData()
}
(onStart用于在应用启动时从SqlLite读取数据,以防没有互联网且Firestore无法访问的情况。)
这完美无瑕且非常快速,一旦我在Firestore控制台中更新数据,就可以在设备中看到UI更新。