本文概述
如果你没有为Android项目选择合适的架构, 那么随着代码库的增长和团队的扩大, 你将很难维护它。
这不仅是Android MVVM教程。在本文中, 我们将结合使用MVVM(模型-视图-视图模型或有时被程式化的”视图模型模式”)与Clean Architecture。我们将看到如何使用此体系结构编写解耦的, 可测试的和可维护的代码。
为什么MVVM具有清洁架构?
MVVM将你的视图(即活动和片段)与业务逻辑分开。 MVVM对于小型项目就足够了, 但是当你的代码库变得巨大时, 你的ViewModels开始膨胀。责任分工变得困难。
在这种情况下, 带有Clean Architecture的MVVM相当不错。在分离代码库的职责方面, 它迈出了进一步的一步。它清楚地抽象了可以在你的应用程序中执行的动作的逻辑。
注意:你也可以将”干净架构”与”模型视图呈现器”(MVP)架构结合使用。但是, 由于Android体系结构组件已经提供了内置的ViewModel类, 因此我们将通过MVP使用MVVM-无需MVVM框架!
使用清洁架构的优势
- 与普通的MVVM相比, 你的代码更易于测试。
- 你的代码进一步分离(最大优势)。
- 包结构甚至更易于浏览。
- 该项目甚至更易于维护。
- 你的团队可以更快地添加新功能。
清洁建筑的缺点
- 它的学习曲线有些陡峭。所有层如何协同工作可能需要一些时间来理解, 特别是如果你来自简单MVVM或MVP之类的模式。
- 它增加了很多额外的类, 因此对于低复杂度的项目而言并不理想。
我们的数据流将如下所示:
我们的业务逻辑与UI完全分离。它使我们的代码非常易于维护和测试。
我们将要看到的示例非常简单。它允许用户创建新帖子, 并查看他们创建的帖子列表。为了简单起见, 在此示例中, 我没有使用任何第三方库(例如Dagger, RxJava等)。
具有干净架构的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, 因此我们跳过了一些可以尝试进一步改进的内容:
- 使用LiveData或RxJava删除回调并使它更整洁。
- 使用状态来表示你的UI。 (为此, 请查看杰克·沃顿的精彩演讲。)
- 使用Dagger 2注入依赖项。
这是适用于Android应用程序的最佳, 最具扩展性的体系结构之一。希望你喜欢这篇文章, 也期待听到你如何在自己的应用中使用这种方法!
相关:Xamarin形式, MVVMCross和SkiaSharp:跨平台应用程序开发的三位一体