作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
拥有5年以上经验的团队领导和Android企业家, Abhishek开发的应用下载量超过500万次.
如果您没有为您的 Android 项目中,随着代码库的增长和团队的扩展,您将很难维护它.
这不仅仅是一个Android MVVM教程. In this article, 我们将把MVVM (Model-View-ViewModel,有时也称为“ViewModel模式”)与 Clean Architecture. 我们将看到如何使用这种体系结构来编写解耦, testable, and maintainable code.
MVVM分离你的视图(i.e. Activity
s and Fragment
S)从您的业务逻辑. MVVM对于小项目来说已经足够了,但是当你的代码库变得巨大时,你的 ViewModel
s start bloating. 责任分离变得困难.
带有Clean Architecture的MVVM在这种情况下非常好. 它在分离代码库的职责方面更进一步. 它清楚地抽象了可以在应用程序中执行的操作的逻辑.
注意:您也可以将Clean架构与模型-视图-演示者(MVP)架构结合起来. But since Android架构组件 已经提供了内置的 ViewModel
类,我们将使用MVVM而不是mvp -不需要MVVM框架!
我们的数据流看起来像这样:
我们的业务逻辑与UI完全解耦. 它使我们的代码非常容易维护和测试.
我们将要看到的例子非常简单. 它允许用户创建新帖子,并查看由他们创建的帖子列表. 我没有使用任何第三方库(如Dagger, RxJava等).),为简单起见.
代码分为三个独立的层:
我们将在下面详细介绍每一层. 现在,我们得到的包结构看起来像这样:
甚至在我们正在使用的Android应用架构中, 有许多方法可以构建文件/文件夹层次结构. 我喜欢根据特性对项目文件进行分组. 我觉得它简洁明了. 您可以自由选择适合您的任何项目结构.
This includes our Activity
s, Fragment
s, and ViewModel
s. An Activity
应该尽可能的愚蠢吗. 永远不要把你的业务逻辑放进去 Activity
s.
An Activity
will talk to a ViewModel
and a ViewModel
将与域层对话以执行操作. A ViewModel
永远不要直接与数据层对话.
Here we are passing a UseCaseHandler
and two UseCase
s 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)
}
})
}
}
域层包含所有的 use cases of your application. In this example, we have UseCase
, an abstract class. All our UseCase
s 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 UseCase
S是你们之间的调解人 ViewModel
s and Repository
s.
假设将来你决定添加“编辑帖子”功能. 你所要做的就是添加一个新的 EditPost
UseCase
它的所有代码都是完全分离的 UseCase
s. 我们已经见过很多次了:新特性的引入无意中破坏了先前存在的代码. Creating a separate UseCase
极大地避免了这种情况.
当然,你不能百分之百地消除这种可能性,但你肯定可以把它降到最低. 这就是Clean Architecture与其他模式的区别:代码是如此解耦,以至于您可以将每个层视为一个黑盒.
这包含域层可以使用的所有存储库. 这一层向外部类公开数据源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.
我将通过一个示例来解释依赖注入. 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的介绍.
我们这个项目的目的是通过Clean Architecture来理解MVVM, 所以我们跳过了一些你可以尝试进一步改进的东西:
这是Android应用程序中最好和最具可扩展性的架构之一. 我希望你喜欢这篇文章, 我期待听到你们是如何在自己的应用中使用这种方法的!
Android架构是你构建Android项目代码的方式,这样你的代码是可伸缩的,易于维护. 开发人员花在维护项目上的时间比最初构建项目的时间要多, 因此,遵循适当的体系结构模式是有意义的.
In Android, MVC指的是默认模式,其中Activity充当控制器,XML文件充当视图. MVVM将Activity类和XML文件都视为视图, 和ViewModel类是您编写业务逻辑的地方. 它完全将应用程序的UI与其逻辑分离开来.
在MVP中,演示者知道视图,视图也知道演示者. 它们通过一个界面相互作用. 在MVVM中,只有视图知道视图模型. 视图模型不知道视图.
一是关注点分离.e. 您的业务逻辑、UI和数据模型应该位于不同的位置. 另一个是代码的解耦:每段代码都应该充当一个黑盒,以便更改类中的任何内容都不会对代码库的其他部分产生任何影响.
Robert C. Martin的“干净架构”是一种模式,它允许您将与数据的交互分解为称为“用例”的更简单的实体.“它非常适合编写解耦代码.
大多数应用程序保存和检索数据,要么从本地存储,要么从远程服务器. Android存储库是决定数据应该来自服务器还是本地存储的类, 将存储逻辑与外部类解耦.
Located in Gurugram, Haryana, India
Member since October 11, 2018
拥有5年以上经验的团队领导和Android企业家, Abhishek开发的应用下载量超过500万次.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.