使用带有干净架构的MVVM的更好的Android应用

本文概述

如果你没有为Android项目选择合适的架构, 那么随着代码库的增长和团队的扩大, 你将很难维护它。

这不仅是Android MVVM教程。在本文中, 我们将结合使用MVVM(模型-视图-视图模型或有时被程式化的”视图模型模式”)与Clean Architecture。我们将看到如何使用此体系结构编写解耦的, 可测试的和可维护的代码。

为什么MVVM具有清洁架构?

MVVM将你的视图(即活动和片段)与业务逻辑分开。 MVVM对于小型项目就足够了, 但是当你的代码库变得巨大时, 你的ViewModels开始膨胀。责任分工变得困难。

在这种情况下, 带有Clean Architecture的MVVM相当不错。在分离代码库的职责方面, 它迈出了进一步的一步。它清楚地抽象了可以在你的应用程序中执行的动作的逻辑。

注意:你也可以将”干净架构”与”模型视图呈现器”(MVP)架构结合使用。但是, 由于Android体系结构组件已经提供了内置的ViewModel类, 因此我们将通过MVP使用MVVM-无需MVVM框架!

使用清洁架构的优势

  • 与普通的MVVM相比, 你的代码更易于测试。
  • 你的代码进一步分离(最大优势)。
  • 包结构甚至更易于浏览。
  • 该项目甚至更易于维护。
  • 你的团队可以更快地添加新功能。

清洁建筑的缺点

  • 它的学习曲线有些陡峭。所有层如何协同工作可能需要一些时间来理解, 特别是如果你来自简单MVVM或MVP之类的模式。
  • 它增加了很多额外的类, 因此对于低复杂度的项目而言并不理想。

我们的数据流将如下所示:

具有Clean Architecture的MVVM的数据流。数据从View到ViewModel到Domain到Data Repository,再到数据源(本地或远程)。

我们的业务逻辑与UI完全分离。它使我们的代码非常易于维护和测试。

我们将要看到的示例非常简单。它允许用户创建新帖子, 并查看他们创建的帖子列表。为了简单起见, 在此示例中, 我没有使用任何第三方库(例如Dagger, RxJava等)。

具有干净架构的MVVM的各层

该代码分为三个单独的层:

  1. 表示层
  2. 域层
  3. 资料层

我们将在下面详细介绍每个图层。现在, 我们生成的包结构如下所示:

具有Clean Architecture软件包结构的MVVM。

即使在我们使用的Android应用程序架构中, 也有许多方法可以构建文件/文件夹层次结构。我喜欢根据功能对项目文件进行分组。我觉得它简洁明了。你可以自由选择适合你的项目结构。

表示层

这包括我们的活动, 片段和视图模型。一个活动应该尽可能的愚蠢。切勿将业务逻辑放入”活动”中。

活动将与ViewModel对话, 而ViewModel将与域层对话以执行操作。 ViewModel从不直接与数据层对话。

在这里, 我们将一个UseCaseHandler和两个UseCases传递给我们的ViewModel。我们将在稍后详细介绍, 但是在这种架构中, UseCase是定义ViewModel与数据层交互方式的一项操作。

这是我们Kotlin代码的外观:

class PostListViewModel(
        val useCaseHandler: UseCaseHandler, val getPosts: GetPosts, val savePost: SavePost): ViewModel() {


    fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        val requestValue = GetPosts.RequestValues(userId)
        useCaseHandler.execute(getPosts, requestValue, object :
        UseCase.UseCaseCallback<GetPosts.ResponseValue> {
            override fun onSuccess(response: GetPosts.ResponseValue) {
                callback.onPostsLoaded(response.posts)
            }

            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

    fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) {
        val requestValues = SavePost.RequestValues(post)
        useCaseHandler.execute(savePost, requestValues, object :
        UseCase.UseCaseCallback<SavePost.ResponseValue> {
            override fun onSuccess(response: SavePost.ResponseValue) {
                callback.onSaveSuccess()
            }
            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

}

域层

域层包含应用程序的所有用例。在此示例中, 我们有一个UseCase, 一个抽象类。我们所有的UseCases都将扩展此类。

abstract class UseCase<Q : UseCase.RequestValues, P : UseCase.ResponseValue> {

    var requestValues: Q? = null

    var useCaseCallback: UseCaseCallback<P>? = null

    internal fun run() {
        executeUseCase(requestValues)
    }

    protected abstract fun executeUseCase(requestValues: Q?)

    /**
     * Data passed to a request.
     */
    interface RequestValues

    /**
     * Data received from a request.
     */
    interface ResponseValue

    interface UseCaseCallback<R> {
        fun onSuccess(response: R)
        fun onError(t: Throwable)
    }
}

UseCaseHandler处理UseCase的执行。从数据库或远程服务器获取数据时, 切勿阻塞UI。在这里, 我们决定在后台线程上执行UseCase并在主线程上接收响应。

class UseCaseHandler(private val mUseCaseScheduler: UseCaseScheduler) {

    fun <T : UseCase.RequestValues, R : UseCase.ResponseValue> execute(
            useCase: UseCase<T, R>, values: T, callback: UseCase.UseCaseCallback<R>) {
        useCase.requestValues = values
        useCase.useCaseCallback = UiCallbackWrapper(callback, this)

        mUseCaseScheduler.execute(Runnable {
            useCase.run()
        })
    }

    private fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>) {
        mUseCaseScheduler.notifyResponse(response, useCaseCallback)
    }

    private fun <V : UseCase.ResponseValue> notifyError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
        mUseCaseScheduler.onError(useCaseCallback, t)
    }

    private class UiCallbackWrapper<V : UseCase.ResponseValue>(
    private val mCallback: UseCase.UseCaseCallback<V>, private val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback<V> {

        override fun onSuccess(response: V) {
            mUseCaseHandler.notifyResponse(response, mCallback)
        }

        override fun onError(t: Throwable) {
            mUseCaseHandler.notifyError(mCallback, t)
        }
    }

    companion object {

        private var INSTANCE: UseCaseHandler? = null
        fun getInstance(): UseCaseHandler {
            if (INSTANCE == null) {
                INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler())
            }
            return INSTANCE!!
        }
    }
}

顾名思义, GetPosts UseCase负责获取用户的所有帖子。

class GetPosts(private val mDataSource: PostDataSource) :
UseCase<GetPosts.RequestValues, GetPosts.ResponseValue>() {

    protected override fun executeUseCase(requestValues: GetPosts.RequestValues?) {
        mDataSource.getPosts(requestValues?.userId ?: -1, object :
        PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List<Post>) {
                val responseValue = ResponseValue(posts)
                useCaseCallback?.onSuccess(responseValue)
            }
            override fun onError(t: Throwable) {
                // Never use generic exceptions. Create proper exceptions. Since
                // our use case is different we will go with generic throwable
                useCaseCallback?.onError(Throwable("Data not found"))
            }
        })
    }
    class RequestValues(val userId: Int) : UseCase.RequestValues
    class ResponseValue(val posts: List<Post>) : UseCase.ResponseValue
}

UseCases的目的是在你的ViewModel和存储库之间充当中介。

假设你将来决定添加”编辑帖子”功能。你所要做的就是添加一个新的EditPost UseCase, 其所有代码将完全分开, 并与其他UseCases分离。我们已经多次看到它:引入了新功能, 它们无意间破坏了已有的代码。创建一个单独的UseCase可以极大地避免这种情况。

当然, 你无法100%消除这种可能性, 但是你可以将其最小化。这就是Clean Architecture与其他模式的区别:代码是如此分离, 以至于你可以将每一层都视为黑匣子。

数据层

它具有域层可以使用的所有存储库。该层将数据源API公开给外部类:

interface PostDataSource {

    interface LoadPostsCallback {
        fun onPostsLoaded(posts: List<Post>)
        fun onError(t: Throwable)
    }

    interface SaveTaskCallback {
        fun onSaveSuccess()
        fun onError(t: Throwable)
    }

    fun getPosts(userId: Int, callback: LoadPostsCallback)
    fun savePost(post: Post)
}

PostDataRepository实现PostDataSource。它决定我们是从本地数据库还是从远程服务器获取数据。

class PostDataRepository private constructor(
        private val localDataSource: PostDataSource, private val remoteDataSource: PostDataSource): PostDataSource {

    companion object {
        private var INSTANCE: PostDataRepository? = null
        fun getInstance(localDataSource: PostDataSource, remoteDataSource: PostDataSource): PostDataRepository {
            if (INSTANCE == null) {
                INSTANCE = PostDataRepository(localDataSource, remoteDataSource)
            }
            return INSTANCE!!
        }
    }
    var isCacheDirty = false
    override fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        if (isCacheDirty) {
            getPostsFromServer(userId, callback)
        } else {
            localDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback {
                override fun onPostsLoaded(posts: List<Post>) {
                    refreshCache()
                    callback.onPostsLoaded(posts)
                }
                override fun onError(t: Throwable) {
                    getPostsFromServer(userId, callback)
                }
            })
        }
    }
    override fun savePost(post: Post) {
        localDataSource.savePost(post)
        remoteDataSource.savePost(post)
    }
    private fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) {
        remoteDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List<Post>) {
                refreshCache()
                refreshLocalDataSource(posts)
                callback.onPostsLoaded(posts)
            }
            override fun onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }
    private fun refreshLocalDataSource(posts: List<Post>) {
        posts.forEach {
            localDataSource.savePost(it)
        }
    }
    private fun refreshCache() {
        isCacheDirty = false
    }
}

该代码大部分是不言自明的。此类具有两个变量, localDataSource和remoteDataSource。它们的类型是PostDataSource, 所以我们不在乎它们是如何在后台实际实现的。

以我个人的经验, 这种架构被证明是无价的。在我的一个应用程序中, 我从后端开始使用Firebase, 这对于快速构建你的应用程序非常有用。我知道最终我将不得不转移到我自己的服务器上。

完成后, 我要做的就是更改RemoteDataSource中的实现。经历了如此巨大的变化后, 我也无需接触任何其他课程。这就是解耦代码的优势。更改任何给定的类不应影响代码的其他部分。

我们拥有一些额外的课程:

interface UseCaseScheduler {

    fun execute(runnable: Runnable)

    fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>)

    fun <V : UseCase.ResponseValue> onError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable)
}


class UseCaseThreadPoolScheduler : UseCaseScheduler {

    val POOL_SIZE = 2

    val MAX_POOL_SIZE = 4

    val TIMEOUT = 30

    private val mHandler = Handler()

    internal var mThreadPoolExecutor: ThreadPoolExecutor

    init {
        mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(), TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE))
    }

    override fun execute(runnable: Runnable) {
        mThreadPoolExecutor.execute(runnable)
    }

    override fun <V : UseCase.ResponseValue> notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback<V>) {
        mHandler.post { useCaseCallback.onSuccess(response) }
    }

    override fun <V : UseCase.ResponseValue> onError(
            useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
        mHandler.post { useCaseCallback.onError(t) }
    }

}

UseCaseThreadPoolScheduler负责使用ThreadPoolExecuter异步执行任务。

class ViewModelFactory : ViewModelProvider.Factory {


    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass == PostListViewModel::class.java) {
            return PostListViewModel(
                    Injection.provideUseCaseHandler()
                    , Injection.provideGetPosts(), Injection.provideSavePost()) as T
        }
        throw IllegalArgumentException("unknown model class $modelClass")
    }

    companion object {
        private var INSTANCE: ViewModelFactory? = null
        fun getInstance(): ViewModelFactory {
            if (INSTANCE == null) {
                INSTANCE = ViewModelFactory()
            }
            return INSTANCE!!
        }
    }
}

这是我们的ViewModelFactory。你必须创建它才能在ViewModel构造函数中传递参数。

依赖注入

我将通过一个示例来说明依赖项注入。如果你看一下我们的PostDataRepository类, 它有两个依赖项, LocalDataSource和RemoteDataSource。我们使用Injection类将这些依赖项提供给PostDataRepository类。

注入依赖项有两个主要优点。一种是你可以从中央位置控制对象的实例化, 而不必将其分布在整个代码库中。另一个是, 这将有助于我们为PostDataRepository编写单元测试, 因为现在我们可以将LocalDataSource和RemoteDataSource的模拟版本传递给PostDataRepository构造函数, 而不是实际值。

object Injection {

    fun providePostDataRepository(): PostDataRepository {
        return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource())
    }

    fun provideViewModelFactory() = ViewModelFactory.getInstance()

    fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance()

    fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance()

    fun provideGetPosts() = GetPosts(providePostDataRepository())

    fun provideSavePost() = SavePost(providePostDataRepository())

    fun provideUseCaseHandler() = UseCaseHandler.getInstance()
}

注意:我更喜欢在复杂项目中使用Dagger 2进行依赖注入。但是, 由于其学习曲线极其陡峭, 因此超出了本文的范围。因此, 如果你有兴趣进一步深入学习, 我强烈推荐Hari Vignesh Jayapalan撰写的Dagger 2简介。

具有干净架构的MVVM:牢固的组合

这个项目的目的是了解带有Clean Architecture的MVVM, 因此我们跳过了一些可以尝试进一步改进的内容:

  1. 使用LiveData或RxJava删除回调并使它更整洁。
  2. 使用状态来表示你的UI。 (为此, 请查看杰克·沃顿的精彩演讲。)
  3. 使用Dagger 2注入依赖项。

这是适用于Android应用程序的最佳, 最具扩展性的体系结构之一。希望你喜欢这篇文章, 也期待听到你如何在自己的应用中使用这种方法!

相关:Xamarin形式, MVVMCross和SkiaSharp:跨平台应用程序开发的三位一体

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?