让 Clean 拯救大型项目

本文旨在把 MVVM 与 Clean 架构结合起来,指导我们怎样使用此架构来编写解耦、可测试和可维护的代码。

为啥要把 Clean 和 MVVM 结合起来

MVVM 旨在把视图(Activity / Fragments)和业务逻辑(ViewModel)拆分出来,这对小型项目已经足够了,但随着代码的增长,ViewModel 会逐渐变得臃肿庞大,对业务逻辑进行职责分离会变得越发吃力。

在这种情况下,MVVM 结合 Clean Architecture 便登场了,它进一步分离了代码的职责,明确地抽象了程序执行的操作逻辑。

注意:其实也可以将 Clean Architecture 与 model-view-presenter (MVP) 架构结合起来。但由于 Android Jetpack 已经提供了内置的 ViewModel,因此我们将使用 MVVM 而不是 MVP。

使用 Clean 带来的优势

  • 代码进一步解耦(最大的优势)
  • 比 MVVM 更容易测试
  • 包结构导航变得容易
  • 工程容易维护
  • 可以更快的添加新 features

Clean 架构带来的缺点

  • 稍微陡峭的学习曲线,所有层如何协同工作可能需要一些时间来理解,特别是如果我们之前基于简单的 MVVM or MVP 模式。
  • 添加了很多额外的类,这对于低复杂度的项目来说并不理想。

data flow

接下来举个例子,允许用户创建新帖子并查看他们创建的帖子列表。为了简单起见,示例中没有使用其他库(如 Hilt、Coroutine 等)。

具有简洁架构的 MVVM 各层,代码分为三个独立的层:

  • 展示层 Present Layer
  • 领域层 Domain Layer
  • 数据层 Data Layer

下面详细介绍每一层。目前,例子中生成的包结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
├── UseCase.kt
├── UseCaseHandler.kt
├── UseCasesScheduler.kt
├── UseCasesThreadPoolScheduler.kt
├── data
│   ├── model
│   │   └── Post.kt
│   └── source
│   ├── PostDataRepository.kt
│   ├── PostDataSource.kt
│   ├── local
│   │   └── LocalDataSource.kt
│   └── remote
│   └── RemoteDataSource.kt
├── ui
│   ├── domain
│   │   ├── GetPosts.kt
│   │   └── SavePost.kt
│   └── postlist
│   ├── PostListActivity.kt
│   ├── PostListAdapter.kt
│   └── PostListViewModel.kt
└── utils
├── Injection.kt
└── ViewModelFactory.kt

其实还有很多方法来构建文件/文件夹层次结构。平时更偏好根据功能对项目文件进行分组。这样简洁明了。

Present Layer

Present Layer 包括 Activity、Fragment 和 ViewModel。其中 Activity 应该尽可能单一。永远不要将业务逻辑放在 Activity 当中。
Activity 与 ViewModel 会话,ViewModel 将与 Domain Layer 执行操作。 ViewModel 从不直接与 Data Layer 会话。
这里将一个 UseCaseHandler 和两个 UseCases 传递给的 ViewModel。在这个架构中,UseCase 是一个定义 ViewModel 如何与数据层交互的操作。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)
}
}
)
}

}

Domain Layer

Domain Layer 包含应用程序的所有用例。在此示例中,有 UseCase,一个抽象类。所有的 UseCase 都将扩展这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 并在主线程上接收响应的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 负责获取用户的所有帖子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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) {
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 架构与其他模式的区别:代码如此解耦,以至于可以将每一层视为黑匣子。

数据层

该层向外部类公开数据源 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。它决定是从本地数据库还是远程服务器获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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,所以不用关心它们实际上是如何实现的。

如果想要调整远程数据源,所要做的就是更改 RemoteDataSource 中的实现。这样不必接触任何其他类。这就是解耦代码的优点。更改任何给定的类不应影响代码的其他部分。

另外还有一些额外的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 异步执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 的 Mock 版本传递给 PostDataRepository 构造函数而不是实际值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()
}

强烈建议使用 Hilt,超出了本文范围,可以参考这里

MVVM 结合 Clean:牢固的组合

这个案例的目的是了解具有 Clean 架构的 MVVM,因此跳过了一些可以尝试进一步改进的内容:

  • 使用 Coroutine 删除回调并使其更加整洁。
  • 使用 Flow 来表示 UI。
  • 使用 Hilt 注入依赖项。

这就是 Android 应用程序最好、最具可扩展性的架构之一!