让 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 包括 ActivityFragmentViewModel。其中 Activity 应该尽可能单一。永远不要将业务逻辑放在 Activity 当中。
ActivityViewModel 会话,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 的目的是成为 ViewModelRepository 之间的中介。

假设将来决定添加 “编辑帖子” 功能。所要做的就是添加一个新的 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
}
}

这个类有两个变量:localDataSourceremoteDataSource。它们的类型是 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 类,它有两个依赖项:LocalDataSourceRemoteDataSource。使用 Injection 类向 PostDataRepository 类提供这些依赖项。

注入依赖有两个主要优点。

  • 一是可以从一个中心位置控制对象的实例化,而不是将其分散到整个代码库。
  • 另一个原因是,这将帮助我们为 PostDataRepository 编写单元测试,因为只需将 LocalDataSourceRemoteDataSource 的 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 应用程序最好、最具可扩展性的架构之一!