作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Abhishek Tyagi的头像

Abhishek Tyagi

拥有5年以上经验的团队领导和Android企业家, Abhishek开发的应用下载量超过500万次.

Expertise

Previously At

Uber
Share

如果您没有为您的 Android 项目中,随着代码库的增长和团队的扩展,您将很难维护它.

这不仅仅是一个Android MVVM教程. In this article, 我们将把MVVM (Model-View-ViewModel,有时也称为“ViewModel模式”)与 Clean Architecture. 我们将看到如何使用这种体系结构来编写解耦, testable, and maintainable code.

为什么使用干净架构的MVVM?

MVVM分离你的视图(i.e. Activitys and FragmentS)从您的业务逻辑. MVVM对于小项目来说已经足够了,但是当你的代码库变得巨大时,你的 ViewModels start bloating. 责任分离变得困难.

带有Clean Architecture的MVVM在这种情况下非常好. 它在分离代码库的职责方面更进一步. 它清楚地抽象了可以在应用程序中执行的操作的逻辑.

注意:您也可以将Clean架构与模型-视图-演示者(MVP)架构结合起来. But since Android架构组件 已经提供了内置的 ViewModel 类,我们将使用MVVM而不是mvp -不需要MVVM框架!

使用干净架构的优点

  • 您的代码甚至比使用普通的MVVM更容易测试.
  • 您的代码进一步解耦(最大的优势).)
  • 包结构甚至更容易导航.
  • 这个项目甚至更容易维护.
  • 您的团队可以更快地添加新功能.

洁净架构的缺点

  • 它有一个稍微陡峭的学习曲线. 所有这些层是如何协同工作的可能需要一些时间来理解, 特别是如果你是从简单的MVVM或MVP模式来的.
  • 它增加了许多额外的类,因此对于低复杂性的项目来说并不理想.

我们的数据流看起来像这样:

Clean架构下的MVVM数据流. 数据从视图流到视图模型流到域流到数据存储库, 然后连接到数据源(本地或远程).)

我们的业务逻辑与UI完全解耦. 它使我们的代码非常容易维护和测试.

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

具有Clean架构的MVVM的层

代码分为三个独立的层:

  1. Presentation Layer
  2. Domain Layer
  3. Data Layer

我们将在下面详细介绍每一层. 现在,我们得到的包结构看起来像这样:

具有Clean Architecture包结构的MVVM.

甚至在我们正在使用的Android应用架构中, 有许多方法可以构建文件/文件夹层次结构. 我喜欢根据特性对项目文件进行分组. 我觉得它简洁明了. 您可以自由选择适合您的任何项目结构.

The Presentation Layer

This includes our Activitys, Fragments, and ViewModels. An Activity 应该尽可能的愚蠢吗. 永远不要把你的业务逻辑放进去 Activitys.

An Activity will talk to a ViewModel and a ViewModel 将与域层对话以执行操作. A ViewModel 永远不要直接与数据层对话.

Here we are passing a UseCaseHandler and two UseCases to our ViewModel. 我们很快会更详细地讨论这个,但是在这个架构中,a UseCase 一个动作定义了如何 ViewModel 与数据层交互.

Here’s how our Kotlin code looks:

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


    gettallposts (userId: Int,回调:PostDataSource.LoadPostsCallback) {
        val requestValue = GetPosts.RequestValues(userId)
        useCaseHandler.执行(getPosts, requestValue, object)
        UseCase.UseCaseCallback {
            覆盖fun onSuccess(响应:GetPosts.ResponseValue) {
                callback.onPostsLoaded(response.posts)
            }

            重载onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

    post: post,回调:PostDataSource.SaveTaskCallback) {
        val requestValues = SavePost.RequestValues(post)
        useCaseHandler.执行(savePost, requestValues, object)
        UseCase.UseCaseCallback {
            覆盖fun onSuccess(响应:SavePost.ResponseValue) {
                callback.onSaveSuccess()
            }
            重载onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }

}

The Domain Layer

域层包含所有的 use cases of your application. In this example, we have UseCase, an abstract class. All our UseCases will extend this class.

abstract class UseCase {

    var requestValues: Q? = null

    var useCaseCallback: UseCaseCallback

? = null internal fun run() { executeUseCase (requestValues) } executeUseCase(requestValues: Q .?) /** *传递给请求的数据. */ interface RequestValues /** *从请求中接收的数据. */ interface ResponseValue interface UseCaseCallback { 成功的乐趣(回应:R) 调用onError(t: Throwable) } }

And UseCaseHandler handles execution of a UseCase. 当我们从数据库或远程服务器获取数据时,我们不应该阻塞UI. 这是我们决定执行我们的 UseCase 在后台线程上,并在主线程上接收响应.

类UseCaseHandler(私有val musecasesscheduler: usecasesscheduler) {

    fun  execute(
            useCase: UseCase, values: T, callback: UseCase.UseCaseCallback) {
        useCase.requestValues = values
        useCase.useCaseCallback = UiCallbackWrapper(callback, this)

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

    private fun  notifyResponse(response: V,
                                                   useCaseCallback: UseCase.UseCaseCallback) {
        mUseCaseScheduler.useCaseCallback notifyResponse(响应)
    }

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

    private class UiCallbackWrapper(
    private val mCallback: UseCase.UseCaseCallback,
    private val mUseCaseHandler: UseCaseHandler): UseCase.UseCaseCallback {

        覆盖fun onSuccess(响应:V) {
            mUseCaseHandler.mCallback notifyResponse(响应)
        }

        重载onError(t: Throwable) {
            mUseCaseHandler.notifyError (mCallback t)
        }
    }

    companion object {

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

As its name implies, the GetPosts UseCase 负责获取用户的所有帖子.


GetPosts(private val mDataSource: PostDataSource):
UseCase() {

    执行usecase (requestValues: GetPosts.RequestValues?) {
        mDataSource.getPosts(requestValues?.userId ?: -1, object :
        PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List) {
                val responseValue = responseValue (posts)
                useCaseCallback?.onSuccess(responseValue)
            }
            重载onError(t: Throwable) {
                //永远不要使用泛型异常. Create proper exceptions. Since
                //我们的用例是不同的,我们将使用generic throwable
                useCaseCallback?.onError(Throwable("Data not found"))
            }
        })
    }
    类RequestValues(val userId: Int): UseCase.RequestValues
    class ResponseValue(val posts: List) : UseCase.ResponseValue
}

The purpose of the UseCaseS是你们之间的调解人 ViewModels and Repositorys.

假设将来你决定添加“编辑帖子”功能. 你所要做的就是添加一个新的 EditPost UseCase 它的所有代码都是完全分离的 UseCases. 我们已经见过很多次了:新特性的引入无意中破坏了先前存在的代码. Creating a separate UseCase 极大地避免了这种情况.

当然,你不能百分之百地消除这种可能性,但你肯定可以把它降到最低. 这就是Clean Architecture与其他模式的区别:代码是如此解耦,以至于您可以将每个层视为一个黑盒.

The Data Layer

这包含域层可以使用的所有存储库. 这一层向外部类公开数据源API:

接口PostDataSource {

    接口loadpostcallback {
        fun onPostsLoaded(posts: List)
        调用onError(t: Throwable)
    }

    接口SaveTaskCallback {
        fun onSaveSuccess()
        调用onError(t: Throwable)
    }

    getPosts(userId: Int,回调:loadpostcallback)
    fun savePost(post: Post)
}

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

postdatarerepository私有构造函数(
        private val localDataSource: PostDataSource
        private val remoteDataSource: PostDataSource): PostDataSource {

    companion object {
        private var INSTANCE: postdatarerepository? = null
        getInstance(localDataSource: PostDataSource)
        remoteDataSource: PostDataSource): postdatarerepository {
            if (INSTANCE == null) {
                PostDataRepository(localDataSource, remoteDataSource)
            }
            return INSTANCE!!
        }
    }
    var isCacheDirty = false
    getPosts(userId: Int,回调:PostDataSource.LoadPostsCallback) {
        if (isCacheDirty) {
            getPostsFromServer (userId,回调)
        } else {
            localDataSource.getPosts(userId, object: PostDataSource.LoadPostsCallback {
                override fun onPostsLoaded(posts: List) {
                    refreshCache()
                    callback.onPostsLoaded(posts)
                }
                重载onError(t: Throwable) {
                    getPostsFromServer (userId,回调)
                }
            })
        }
    }
    重载好玩的savePost(post: post) {
        localDataSource.savePost(post)
        remoteDataSource.savePost(post)
    }
    getPostsFromServer(userId: Int,回调:PostDataSource.LoadPostsCallback) {
        remoteDataSource.getPosts(userId, object: PostDataSource.LoadPostsCallback {
            override fun onPostsLoaded(posts: List) {
                refreshCache()
                refreshLocalDataSource(职位)
                callback.onPostsLoaded(posts)
            }
            重载onError(t: Throwable) {
                callback.onError(t)
            }
        })
    }
    private fun refreshLocalDataSource(posts: List) {
        posts.forEach {
            localDataSource.savePost(it)
        }
    }
    private fun refreshCache() {
        isCacheDirty = false
    }
}

代码基本上是不言自明的. 这个类有两个变量, localDataSource and remoteDataSource. Their type is PostDataSource,所以我们不关心它们在底层是如何实现的.

在我个人的经验中,这种架构被证明是无价的. In one of my apps, 我从后端Firebase开始,这对于快速构建应用程序非常有用. 我知道,最终我必须改用我自己的服务器.

When I did, 我所要做的就是改变实现 RemoteDataSource. 即使发生了如此巨大的变化,我也不需要接触任何其他课程. 这就是解耦代码的优点. 更改任何给定的类都不应该影响代码的其他部分.

我们有一些额外的课程:

接口usecasesscheduler

    fun execute(runnable:可运行)

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

    fun  onError(
            useCaseCallback: UseCase.UseCaseCallback, t: Throwable)
}


UseCaseThreadPoolScheduler:

    val POOL_SIZE = 2

    val MAX_POOL_SIZE = 4

    val TIMEOUT = 30

    private val mHandler = Handler()

    内部变量mThreadPoolExecutor: ThreadPoolExecutor

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

    重载fun execute(runnable: runnable) {
        mThreadPoolExecutor.execute(runnable)
    }

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

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

}

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


ViewModelFactory: ViewModelProvider.Factory {


    override fun  create(modelClass: Class): T {
        if (modelClass == PostListViewModel::class.java) {
            return PostListViewModel(
                    Injection.provideUseCaseHandler()
                    , Injection.provideGetPosts(),注入.provideSavePost()) as T
        }
        抛出IllegalArgumentException("未知的模型类$modelClass")
    }

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

This is our ViewModelFactory. 你必须创建这个来传递参数 ViewModel constructor.

Dependency Injection

我将通过一个示例来解释依赖注入. If you look at our PostDataRepository 类,它有两个依赖项, LocalDataSource and RemoteDataSource. We use the Injection 类将这些依赖项提供给 PostDataRepository class.

注入依赖有两个主要优点. 一个是,您可以从中心位置控制对象的实例化,而不是将其分散到整个代码库中. 另一个是,这将帮助我们编写单元测试 PostDataRepository 因为现在我们可以传递模拟版本的 LocalDataSource and RemoteDataSource to the PostDataRepository 构造函数而不是实际值.

object Injection {

    调用PostDataRepository {
        返回PostDataRepository.getInstance (provideLocalDataSource (), provideRemoteDataSource ())
    }

    fun provideViewModelFactory() = ViewModelFactory.getInstance()

    PostDataSource = LocalDataSource.getInstance()

    PostDataSource = RemoteDataSource.getInstance()

    provideGetPosts() = GetPosts(providePostDataRepository())

    fun provideSavePost() = SavePost(providespostdatarepository ())

    provideUseCaseHandler() = UseCaseHandler.getInstance()
}

注意:我更喜欢在复杂项目中使用匕首2进行依赖注入. 但是由于其极其陡峭的学习曲线,它超出了本文的范围. 所以如果你有兴趣深入了解,我强烈推荐 Hari Vignesh Jayapalan对匕首2的介绍.

MVVM与干净的架构:一个坚实的组合

我们这个项目的目的是通过Clean Architecture来理解MVVM, 所以我们跳过了一些你可以尝试进一步改进的东西:

  1. 使用LiveData或RxJava删除回调,使其更整洁.
  2. 使用状态来表示UI. (For that, check out 杰克·沃顿的精彩演讲.)
  3. 使用匕首2注入依赖项.

这是Android应用程序中最好和最具可扩展性的架构之一. 我希望你喜欢这篇文章, 我期待听到你们是如何在自己的应用中使用这种方法的!

Understanding the basics

  • 什么是Android架构?

    Android架构是你构建Android项目代码的方式,这样你的代码是可伸缩的,易于维护. 开发人员花在维护项目上的时间比最初构建项目的时间要多, 因此,遵循适当的体系结构模式是有意义的.

  • MVC和MVVM的区别是什么?

    In Android, MVC指的是默认模式,其中Activity充当控制器,XML文件充当视图. MVVM将Activity类和XML文件都视为视图, 和ViewModel类是您编写业务逻辑的地方. 它完全将应用程序的UI与其逻辑分离开来.

  • MVP和MVVM有什么区别?

    在MVP中,演示者知道视图,视图也知道演示者. 它们通过一个界面相互作用. 在MVVM中,只有视图知道视图模型. 视图模型不知道视图.

  • Android架构的关键组件是什么?

    一是关注点分离.e. 您的业务逻辑、UI和数据模型应该位于不同的位置. 另一个是代码的解耦:每段代码都应该充当一个黑盒,以便更改类中的任何内容都不会对代码库的其他部分产生任何影响.

  • 什么是干净的建筑?

    Robert C. Martin的“干净架构”是一种模式,它允许您将与数据的交互分解为称为“用例”的更简单的实体.“它非常适合编写解耦代码.

  • 什么是Android存储库?

    大多数应用程序保存和检索数据,要么从本地存储,要么从远程服务器. Android存储库是决定数据应该来自服务器还是本地存储的类, 将存储逻辑与外部类解耦.

聘请Toptal这方面的专家.
Hire Now
Abhishek Tyagi的头像
Abhishek Tyagi

Located in Gurugram, Haryana, India

Member since October 11, 2018

About the author

拥有5年以上经验的团队领导和Android企业家, Abhishek开发的应用下载量超过500万次.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Uber

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.