0

我正在使用 Android Architecture Components 编写一个应用程序,最初基于著名的文章,但是现在已经过时且不准确,因此基于其他文档、文章和视频,我使用最新的组件构建了一些东西,结果在一个非常简单的架构,代码很少。

这个想法是应用程序从其表为空开始,然后从 Firestore 数据库中读取以获取其数据,将数据存储在本地 SqlLite 数据库中(使用 Room)并显示更新的数据。每当在 Firestore 上更新数据时,都应该在 SqlLite 中更新数据并更新 UI。

但是,我的 UI(现在只是一个文本框)仅在应用程序启动时更新,并且在数据库被修改后永远不会更新。

波特罗道

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

按钮视图模型

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),我什至检查了 Fragment 和 ViewModel 本身的 ViewModel 实例是否相同,并且通过 Room 的持久性是正确的;当我更新 Firestore 时,新数据实际上被保存到 SqlLite 中。此外,logcat 中没有异常或错误。但是UI没有更新。

4

1 回答 1

0

所以,我设法找到了一种方法来完成这项工作,尽管方式不同。我的想法是每当我写信给 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 中将 Flow 转换为 liveData:

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

(onStart 用于在应用启动时从 SqlLite 读取数据,以防没有互联网且 Firestore 无法访问)。

这可以完美运行并且速度非常快,只要我在 Firestore 控制台中更新数据,我就可以在设备中看到 UI 更新。

于 2020-08-17T22:08:33.347 回答