在Firebase中使用Android MVVM模式

应用设计模式 (Application Design Patterns)

In this post, I will describe how I use the MVVM pattern with Firebase. This post assumes you are familiar with the MVVM architecture and have some experience with LiveData and coroutines. All code is written in Kotlin.

在本文中,我将介绍如何在Firebase中使用MVVM模式。 这篇文章假定您熟悉MVVM架构,并且对LiveData协程有一定的经验。 所有代码都是用Kotlin编写的。

If you have read about MVVM with Android, the below diagram is what you are most likely familiar with.

如果您已阅读有关Android上的MVVM的信息,则下图可能是您最熟悉的。

However, with Cloud Firestore, we can remove the last two parts of it. This is because Firestore provides its own local cache. An (additional) local cache is not needed or recommended. This means we can remove the model and remote data source and combine them in a single repository class as shown in the below diagram.

但是,使用Cloud Firestore ,我们可以删除其中的最后两个部分。 这是因为Firestore提供了自己的本地缓存。 不需要或不建议使用(其他)本地缓存。 这意味着我们可以删除模型和远程数据源,并将它们组合在单个存储库类中,如下图所示。

Image for post
Source: Modification of the above image with my very limited editing skills
资料来源:以我非常有限的编辑技巧修改了上图

For any given part of the app, I end up using four different classes:

对于应用程序的任何给定部分,我最终使用四个不同的类:

  1. Activity/Fragment

    活动/片段
  2. ViewModel

    ViewModel

  3. A single firestore service object

    单个Firestore服务对象
  4. Data class (Object representation of the required data)

    数据类(所需数据的对象表示)

资料类别 (Data Class)

Let’s start bottom-up, my data class is a standard Kotlin data class with a companion function to convert document snapshots to the profile object. I know the standard .toObject(class) function can help me with this, but I prefer writing my own function for this. This helps better handle errors and provide default values. In some cases, it even helps shift from standard data types to enums for better representing certain elements.

让我们自下而上地开始,我的数据类是标准的Kotlin数据类,它具有将文档快照转换为配置文件对象的配套功能。 我知道标准的.toObject(class)函数可以帮助我解决这个问题,但是我更喜欢为此编写自己的函数。 这有助于更好地处理错误并提供默认值。 在某些情况下,它甚至可以帮助从标准数据类型转换为枚举,以更好地表示某些元素。

A standard user profile data class can look something like:-

标准的用户个人资料数据类可能类似于:

@Parcelize
data class User(val userId: String, //Document ID is actually the user id
                     val name: String,
                     val bio: String,
                     val imageUrl: String) : Parcelable {


    companion object {
        fun DocumentSnapshot.toUser(): User? {
            try {
                val name = getString("name")!!
                val imageUrl = getString("profile_image")!!
                val bio = getString("user_bio")!!
                return User(id, name, bio, imageUrl)
            } catch (e: Exception) {
                Log.e(TAG, "Error converting user profile", e)
                FirebaseCrashlytics.getInstance().log("Error converting user profile")
                FirebaseCrashlytics.getInstance().setCustomKey("userId", id)
                FirebaseCrashlytics.getInstance().recordException(e)
                return null
            }
        }
        private const val TAG = "User"
    }
}

You will notice the added Crashlytics reporting. This is something I found useful after working on a production database where data types were often mixed up. Further, by returning null, I can tell my Firebase service function that something was wrong with this particular document and we can ignore it if needed.

您会注意到添加的Crashlytics报告。 在处理经常混合数据类型的生产数据库后,我发现这很有用。 此外,通过返回null ,我可以告诉Firebase服务函数此特定文档有问题,如果需要,我们可以忽略它。

Firestore服务对象 (Firestore Service object)

For hosting our database code, we use a Kotlin object. This makes our Firebase Service a singleton, making only a single instance of this service available. This means we can easily access the functions defined here from anywhere in our code.

为了托管我们的数据库代码,我们使用Kotlin对象 。 这使我们的Firebase服务成为单例,仅使该服务的单个实例可用。 这意味着我们可以轻松地从代码中的任何位置访问此处定义的功能。

A simple Firebase service object with a get function could be something like:

具有get函数的简单Firebase服务对象可能类似于:

object FirebaseProfileService {
    private const val TAG = "FirebaseProfileService"
    suspend fun getProfileData(userId: String): User? {
        val db = FirebaseFirestore.getInstance()
        return try {
            db.collection("users")
                    .document(userId).get().await().toUser()
        } catch (e: Exception) {
            Log.e(TAG, "Error getting user details", e)
            FirebaseCrashlytics.getInstance().log("Error getting user details")
            FirebaseCrashlytics.getInstance().setCustomKey("user id", xpertSlug)
            FirebaseCrashlytics.getInstance().recordException(e)
            null
        }
    }
}

I am using suspend functions as they can easily be launched from any coroutine. They provide the flexibility of synchronous programming with async code. We will be calling this function from our view model which already has a coroutine scope viewModelScope.

我正在使用暂停功能,因为它们可以从任何协程轻松启动。 它们提供了使用异步代码进行同步编程的灵活性。 我们将从已经具有协程作用域viewModelScope视图模型中调用此函数。

Notice that I am again using try-catch blocks. This time, it helps determine errors like missing documents/invalid collection names. Further, it makes use of the toUser() extension function we wrote earlier. This makes the code both easier to read and more reusable.

注意,我再次使用try-catch块。 这次,它有助于确定错误,例如缺少文档/无效的集合名称。 此外,它利用了我们先前编写的toUser()扩展功能。 这使得代码既易于阅读又可重用。

Now, let’s say we need to get a collection of documents - for example a list of friends. You can add another function to the same object to return a list of users.

现在,假设我们需要获取文档的集合-例如朋友列表。 您可以向同一对象添加另一个功能以返回用户列表。

suspend fun getFriends(userId: String): List<User> {
    val db = FirebaseFirestore.getInstance()
    return try {
        db.collection("users")
                .document(userId)
                .collection("friends").get().await()
                .documents.mapNotNull { it.toUser() }
    } catch (e: Exception) {
        Log.e(TAG, "Error getting user friends", e)
        FirebaseCrashlytics.getInstance().log("Error getting user friends")
        FirebaseCrashlytics.getInstance().setCustomKey("user id", xpertSlug)
        FirebaseCrashlytics.getInstance().recordException(e)
        emptyList()
    }
}

Notice the mapNotNull function? It is really useful in these situations. You want to avoid showing malformed documents and our original toUser function returns a null for every document not confirming to our structure. We simply return an empty list in case we face some errors. The user doesn’t need to know what exactly went wrong. Only you need that information.

注意mapNotNull函数吗? 在这些情况下,它确实很有用。 您要避免显示格式错误的文档,并且我们原始的toUser函数针对未确认我们结构的每个文档都返回null。 如果遇到一些错误,我们只是返回一个空列表。 用户不需要知道到底出了什么问题。 只有您需要该信息。

流量 (Flows)

Another topic I would like to touch on is Kotlin flows. Flows are kind of like LiveData but don’t run until someone is collecting the flow. Then they emit values to the collector. Flow also adheres to Coroutine cancellation principles and can be extended with operators for complex tasks.

我想谈谈的另一个主题是Kotlin Flow 。 流有点像LiveData,但要等到有人收集流后再运行。 然后它们向收集器发出值。 Flow还遵循协程取消原则,并且可以与操作员一起扩展以执行复杂任务。

In Firestore terms, a flow can be used with a snapshot listener. We can attach a listener when the flow is first collected and then detach it when the flow is cancelled. This can be really useful for things like live feeds, chat applications, and apps where some data is being continuously updated.

用Firestore术语来说,流可以与快照侦听器一起使用。 我们可以在第一次收集流时附加一个侦听器,然后在取消该流时分离它。 这对于实时供稿,聊天应用程序以及某些数据正在不断更新的应用程序非常有用。

Note that callback flow, which we are using below, is a part of the experimental coroutines API. So you will need to use the annotation @ExperimentalCoroutinesApi whenever you use this.

请注意,我们在下面使用的回调流程是实验性协程API的一部分。 因此,每当您使用此注释时,都将需要使用注释@ExperimentalCoroutinesApi

A very simplified example of a flow is:

流的一个非常简化的示例是:

fun getPosts(userId: String): Flow<List<Post>> {
    val db = FirebaseFirestore.getInstance()
    return callbackFlow {
        val listenerRegistration = db.collection("users")
                .document(userId)
                .collection("posts")
                .addSnapshotListener { querySnapshot: QuerySnapshot?, firebaseFirestoreException: FirebaseFirestoreException? ->
                    if (firebaseFirestoreException != null) {
                        cancel(message = "Error fetching posts",
                                cause = firebaseFirestoreException)
                        return@addSnapshotListener
                    }
                    val map = querySnapshot.documents.
                            .mapNotNull { it.toPost() }
                    offer(map)
        }
        awaitClose {
            Log.d(TAG, "Cancelling posts listener")
            listenerRegistration.remove()
        }
    }
}

Please note that the above example is very basic. An actual use case can be much more complex.

请注意,以上示例是非常基本的。 实际的用例可能要复杂得多。

Anyways, the main element to note here is the CallbackFlow. A callback flow is used (as the name suggests) to attach a flow to a callback like the Firebase snapshot listener. The basic steps being done here are:

无论如何,这里要注意的主要元素是CallbackFlow 。 回调流(顾名思义)用于将流附加到Firebase快照侦听器之类的回调。 这里要做的基本步骤是:

  1. Creating a listener registration inside a callback flow

    在回调流中创建侦听器注册
  2. Cancelling the registration in case of any error

    发生任何错误时取消注册
  3. Emitting the results via the offer() method

    通过offer()方法发出结果

  4. Calling awaitClose

    呼叫等待关闭

Now, awaitClose needs a special mention here. This single statement keeps this flow active and ensures it waits till it’s closed or cancelled. When it’s closed, we can safely detach the Firebase listener we attached earlier.

现在, awaitClose需要在这里特别提及。 该单个语句使该流保持活动状态,并确保它一直等待到关闭或取消为止。 关闭后,我们可以安全地分离我们先前连接的Firebase侦听器。

视图模型 (ViewModel)

Now comes one of the most important elements of the architecture - The ViewModel. The view model can be thought of as the heart of your app. It connects the data to the UI while handling the logical part of your app.

现在出现了体系结构中最重要的元素之一ViewModel 。 可以将视图模型视为应用程序的核心。 它在处理应用程序逻辑部分的同时将数据连接到UI。

You need to calculate and update a score based on some formula? The view model handles it; You want to generate a random number? Leave it to the view model. This is also the layer where we convert the data from Firebase into our beloved LiveData streams and let the UI handle the rest.

您需要根据一些公式来计算和更新分数吗? 视图模型可以处理它; 您要生成一个随机数吗? 留给视图模型。 这也是我们将Firebase中的数据转换为心爱的LiveData流并让UI处理其余部分的层。

Let’s create a view model for using the above repository we created:-

让我们创建一个视图模型以使用我们创建的上述存储库:

class UserProfileViewModel : ViewModel() {
    private val _userProfile = MutableLiveData<User>()
    val userProfile: LiveData<User> = _userProfile
    private val _posts = MutableLiveData<List<Post>>()
    val posts: LiveData<List<Post>> = _posts
    
    init {
        viewModelScope.launch {
            _userProfile.value = FirebaseProfileService.getProfileData()
            _posts.value = FirebaseProfileService.getPosts()
        }
    }
    //Rest of your viewmodel
}

Many interesting points to note here. Firstly, notice how I am using a private _userProfile and a userProfile differently. The major reason for this is that you don’t want to expose your mutable properties to your activity/fragment. Only the viewModel has access to the mutable live data while only the immutable live data is exposed.

这里有许多有趣的要点。 首先,请注意我如何分别使用私有_userProfileuserProfile 。 这样做的主要原因是您不想将可变属性公开给活动/片段。 只有viewModel可以访问可变实时数据,而只有不可变实时数据才可以访问。

Now, in this example, I have two liveDatas - userProfile and posts. userProfile is an example of a single object while posts is a list of objects. These represent the two types of calls you will mostly be making to Firebase. A single document and a collection.

现在,在此示例中,我有两个liveDatas- userProfilepostsuserProfile是单个对象的示例,而posts是对象列表。 这些代表您将主要对Firebase进行的两种呼叫。 单个文档和一个集合。

Notice the viewModelScope, it is a special CoroutineScope provided by Android for use in your view models. Since our Firebase functions are defined as suspend functions, you can only call them from within a coroutine scope or another suspend function. However, using viewModelScope makes it easy to manage your data. All pending coroutines are automatically cancelled when the view model is destroyed. This helps prevent memory leaks and unnecessary network calls. You can learn more about cancellation in coroutines here.

请注意viewModelScope ,它是Android提供的一种特殊的CoroutineScope ,可在您的视图模型中使用。 由于我们的Firebase函数被定义为暂停函数 ,因此您只能从协程范围或另一个暂停函数中调用它们。 但是,使用viewModelScope可以轻松管理数据。 销毁视图模型后,所有挂起的协程将自动取消。 这有助于防止内存泄漏和不必要的网络调用。 您可以在此处了解有关协程取消的更多信息。

If you need your list to be updated, there are two ways. The first one is using a flow with a listener (as shown above) and you can use the .asLiveData() method to convert your flow to a liveData. The other is manually querying the database again and adding the new documents to your list. For this, you can take help from the paging library. Firebase allows you to set conditions such as startafter and limit for your queries where you can pass the last document you received and receive the next N documents.

如果您需要更新列表,则有两种方法。 第一个是使用带有侦听器的流(如上所示),您可以使用.asLiveData()方法将流转换为liveData。 另一种是再次手动查询数据库并将新文档添加到列表中。 为此,您可以从分页库中获取帮助。 Firebase允许您设置条件,例如startafter和查询limit ,您可以在其中传递您收到的最后一个文档并接收下一个N个文档。

Normally, I would store a private mutable list in the viewModel along with the live data abstractions for this particular case. When I get the new documents, I can add them to my mutable list and then update my live data with the new list.

通常,对于这种特殊情况,我会将私有可变列表与实时数据抽象一起存储在viewModel中。 收到新文档后,可以将它们添加到可变列表中,然后使用新列表更新实时数据。

活动和片段 (Activities & Fragments)

Now we come to our final layer in this architecture - our UI layer.

现在,我们进入该体系结构的最后一层-UI层。

You are already familiar with this layer. I will only be highlighting how to use our view model with this.

您已经熟悉此层。 我将仅强调如何与此一起使用我们的视图模型。

class UserProfileFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding = LayoutUserProfileBinding.inflate(inflater, container, false)
        val viewModel = ViewModelProvider(this)[UserProfileViewModel::class.java]
        /*
        * Initialize your views, adapter etc.
        */
        viewModel.userProfile.observe(viewLifecycleOwner, Observer {
            binding.user = it
        })
        viewModel.posts.observe(viewLifecycleOwner, Observer {
            adapter.submitList(it)
        )}
    }
}

Our first step with our fragment or activity is to initialize the views and then get an instance of our view model using ViewModelProvider. Note that although I am using data binding in the above example, it is not necessary to use data binding in your project. Using it, however, will make your life easier.

我们片段或活动的第一步是初始化视图,然后使用ViewModelProvider获取我们的视图模型的实例。 请注意,尽管我在上面的示例中使用了数据绑定,但是没有必要在项目中使用数据绑定。 但是,使用它会使您的生活更轻松。

So coming to our view model, we have two LiveDatas we want to observe from our Fragment. We attach a simple Observer while specifying the owner as viewLifecycleOwner. This allows our LiveData to be automatically managed by the fragment’s lifecycle. When our lifecycle is destroyed, we will automatically stop observing the liveDatas.

因此,在视图模型中,我们要从Fragment观察两个LiveData。 我们在指定所有者为viewLifecycleOwner同时附加了一个简单的Observer 。 这使我们的LiveData可以由片段的生命周期自动管理。 当我们的生命周期被破坏时,我们将自动停止观察liveDatas。

Now, in our observers, we only get the current copy of the data. So, userProfile will have the updated user profile as fetched from Firebase (or null if nothing is fetched yet). posts will have the latest copy of our posts list. We can then send this data to our adapters or use it directly in our views. Just like you normally do. When the data is updated, the observer is called again so you can easily update the UI almost instantaneously. Make sure your observer is suitable for running multiple times even in succession.

现在,在我们的观察者中,我们仅获得数据的当前副本。 因此, userProfile将具有从Firebase获取的更新的用户配置文件(如果未获取任何内容,则为null)。 posts将具有我们的posts列表的最新副本。 然后,我们可以将该数据发送到我们的适配器,或直接在我们的视图中使用它。 就像您平时一样。 数据更新后,将再次调用观察者,因此您可以轻松地几乎立即更新UI。 确保观察者适合连续多次跑步。

Now our UI layer doesn’t need to worry about any implementation detail. This means if your backend logic changes, you can easily make changes to your app without worrying about modifying the UI.

现在,我们的UI层无需担心任何实现细节。 这意味着,如果您的后端逻辑发生了变化,您可以轻松地对应用程序进行更改,而不必担心修改UI。

结论 (Conclusion)

Thank you so much for reading — I hope you learnt something.

非常感谢您的阅读-希望您学到了一些东西。

Architecture components with Kotlin make it easy to manage your app. I have personally used most of the concepts demonstrated here in actual production apps. You can learn more about Jetpack Architecture components on the Guide to app architecture. If you are just getting started with view models, this Codelab might be a good starting point.

Kotlin的架构组件使管理您的应用程序变得容易。 我亲自使用了实际生产应用程序中演示的大多数概念。 您可以在“应用程序体系结构指南”中了解有关Jetpack体系结构组件的更多信息。 如果您只是刚开始使用视图模型,那么此Codelab可能是一个不错的起点。

I must admit this article was highly opinionated and based on my experience dealing with Firebase and MVVM. You might have a different or better approach to handling some of the things I mentioned here. If so, please do leave a comment, I would love to hear more about it!

我必须承认,这篇文章是基于我在Firebase和MVVM方面的经验而引起的高度评价。 您可能有其他或更好的方法来处理我在这里提到的某些问题。 如果是这样,请发表评论,我希望听到更多有关它的信息!

资源资源 (Resources)

翻译自: https://medium.com/firebase-developers/android-mvvm-firestore-37c3a8d65404

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值