使用具有简洁架构的 MVVM 更好的 Android 应用程序
已发表: 2022-03-11如果您没有为您的 Android 项目选择正确的架构,那么随着代码库的增长和团队的扩展,您将很难维护它。
这不仅仅是一个 Android MVVM 教程。 在本文中,我们将结合 MVVM(Model-View-ViewModel 或有时风格化的“ViewModel 模式”)与 Clean Architecture。 我们将看到如何使用这种架构来编写解耦、可测试和可维护的代码。
为什么选择具有干净架构的 MVVM?
MVVM 将您的视图(即Activity和Fragment )与您的业务逻辑分开。 MVVM 对于小型项目来说已经足够了,但是当你的代码库变得庞大时,你的ViewModel就会开始膨胀。 分离职责变得困难。
在这种情况下,具有 Clean Architecture 的 MVVM 非常好。 它在分离代码库的职责方面更进了一步。 它清楚地抽象了可以在您的应用程序中执行的操作的逻辑。
注意:您也可以将 Clean Architecture 与 model-view-presenter (MVP) 架构结合起来。 但是由于 Android 架构组件已经提供了一个内置的ViewModel类,我们将使用 MVVM 而不是 MVP——不需要 MVVM 框架!
使用干净架构的优势
- 你的代码比普通的 MVVM 更容易测试。
- 您的代码进一步解耦(最大的优势。)
- 包结构更易于浏览。
- 该项目更易于维护。
- 您的团队可以更快地添加新功能。
清洁架构的缺点
- 它有一个稍微陡峭的学习曲线。 所有层如何协同工作可能需要一些时间才能理解,特别是如果您来自简单的 MVVM 或 MVP 等模式。
- 它增加了很多额外的类,所以它不适合低复杂度的项目。
我们的数据流将如下所示:
我们的业务逻辑与我们的 UI 完全解耦。 它使我们的代码非常易于维护和测试。
我们将要看到的例子非常简单。 它允许用户创建新帖子并查看他们创建的帖子列表。 为了简单起见,在这个示例中我没有使用任何第三方库(如 Dagger、RxJava 等)。
具有干净架构的 MVVM 层
代码分为三个独立的层:
- 表示层
- 领域层
- 数据层
我们将在下面详细介绍每一层。 现在,我们生成的包结构如下所示:
即使在我们使用的 Android 应用架构中,也有很多方法可以构建文件/文件夹层次结构。 我喜欢根据功能对项目文件进行分组。 我觉得它简洁明了。 您可以自由选择适合您的任何项目结构。
表示层
这包括我们的Activity 、 Fragment和ViewModel 。 Activity应该尽可能的愚蠢。 永远不要将您的业务逻辑放在Activity中。
Activity将与ViewModel对话, ViewModel将与域层对话以执行操作。 ViewModel从不直接与数据层对话。
在这里,我们将一个UseCaseHandler和两个UseCase传递给我们的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 。 我们所有的UseCase都将扩展这个类。
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 } UseCase的目的是成为您的ViewModel和Repository之间的中介。

假设将来您决定添加“编辑帖子”功能。 您所要做的就是添加一个新的EditPost UseCase ,它的所有代码都将与其他UseCase完全分离并解耦。 我们都见过很多次:引入了新功能,它们无意中破坏了现有代码中的某些内容。 创建一个单独的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,因此我们跳过了一些您可以尝试进一步改进它的事情:
- 使用 LiveData 或 RxJava 删除回调并使其更整洁。
- 使用状态来表示您的 UI。 (为此,请查看 Jake Wharton 的精彩演讲。)
- 使用 Dagger 2 注入依赖项。
这是适用于 Android 应用程序的最佳和最具可扩展性的架构之一。 我希望你喜欢这篇文章,我期待听到你如何在自己的应用程序中使用这种方法!
