Android端的架构设计的演进和思考

无架构设计

无架构设计时期,直接基于Activity/Fragment组件开发,分层和特点:

  1. View层:layout布局
    1. 基于XML的有状态View
    2. 无法承载所有View,如RecyclerView
  2. Activity/Fragment:大管家
    1. 初始化View,如Adapter
    2. 直接耦合各种数据源
    3. 与Android平台深度依赖

单元测试分类及意义

上述是常规的黑盒测试,从中可以发现,通过黑盒测试,很难模拟各种边界条件。

好的架构除了可扩展和易维护的特点外,还有一个重大的优点是:方便写单元测试

单元测试的分类:

  1. Local tests:JVM上运行,不依赖真机或模拟器,执行时间最快
  2. Instrumented tests:真机或模拟器上运行,部署麻烦,执行较慢

单元测试的意义:

  1. 模拟黑盒测试难以模拟的边界条件
  2. 代码重构的保障
  3. 最好的文档

单元测试的范围:

  1. 正常和异常的Case,如发生过的Bug,Null处理
  2. 性能测试,如方法耗时

MVC框架

在 MVC框架里,重点是对 M层的分细,实现低耦合。

MVC:

  1. View层:XML 布局文件
  2. C层:Activity/Fragment
    1. 初始化 View,即与View层耦合很大
    2. 依据 UseCase 的过程和结果,更新View,如Loading、Error、Success
  3. M层:UseCase + Repository
    1. UseCase 不依赖 Android 平台
    2. 通过 RxJava 组装各种数据源
    3. Repository 的实现依赖 Android 平台,功能单一

MVP框架

MVP:

  1. View层:XML + Activity/Fragment
  2. P层:Presenter
    1. 与 View 层解藕,接口依赖
    2. 依据 UseCase 的过程和结果,通过View Contract更新View,如Loading、Error、Success

使 Presenter 支持单元测试

MVVM+RxJava框架

MVVM:

  1. View层:XML + Activity/Fragment
  2. VM层:ViewModel
    1. 与 View 层解藕,数据监听,生命周期监听
    2. Fragment之间共享数据
    3. 屏幕旋转时,界面重建,数据保留
    4. 依据 UseCase 的过程和结果,更新LiveData

使用 RxJava 函数式编译,不仅仅是为了方便线程切换,RxJava 带来最大的好处在于在空间和时间两个维度上,对结果进行重排。

通过 MediatorLiveData 也能实现对结果进行重排:

MVVM+LiveData框架

LiveData 本身支持多线程,通过 MediatorLiveData 也能实现对结果的重排,即可以把 M层进行纯 LiveData 改造。

改造成本不大,主要是两个核心点:

  1. LiveDataTask:run(),LiveData实现线程同步
  2. UseCase:
    1. 线程池:io, net, main
    2. MediatorLiveData 组合各种 LiveData

MVVM+LiveData的代码实现

M层

LiveDataTask:

1
2
3
4
5
6
public abstract class LiveDataTask<T> implements Runnable {
protected final MutableLiveData<DataResult<T>> _liveData = new MutableLiveData<>();
public LiveData<DataResult<T>> getLiveData(){
return _liveData;
}
}

ContentDataRepository:

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
public interface ContentDataRepository {
/**
* 从网络请求首页数据
* @param cityDir
*/
LiveDataTask<ContentBean> contentDataFromNet(String cityDir);

/**
* 从缓存中获取首页数据
* @param cityDir
*/
LiveDataTask<ContentBean> contentDataFromCache(String cityDir);

/**
* 从Assets中获取首页数据
* @param cityDir
*/
LiveDataTask<ContentBean> contentDataFromAssets(String cityDir);

/**
* 保存数据到本地缓存
* @param pairData
*/
LiveDataTask<Unit> saveContentDataToCache(ContentBean pairData);
}

GetContentDataUseCase:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class GetContentDataUseCase extends UseCase<GetContentDataUseCase.RequestValues, ContentBean> {

private final ContentDataRepository mContentDataRepository;

public GetContentDataUseCase(
AppExecutors appExecutors,
ContentDataRepository contentDataRepository
) {
super(appExecutors);
this.mContentDataRepository = contentDataRepository;
}

@Override
public LiveData<DataResult<ContentBean>> execute(RequestValues requestValues) {
LiveDataTask<ContentBean> assetsLiveDataTask, cacheLiveDataTask, networkLiveDataTask;

// 三个原始的LiveDataTask
assetsLiveDataTask = mContentDataRepository.contentDataFromAssets(requestValues.cityDir);
cacheLiveDataTask = mContentDataRepository.contentDataFromCache(requestValues.cityDir);
networkLiveDataTask = mContentDataRepository.contentDataFromNet(requestValues.cityDir);

MediatorLiveData<DataResult<ContentBean>> result = new MediatorLiveData<>();

// 显示加载中...
result.setValue(DataResult.loading(null));

// 组合本地级存 与 Assets缓存,逻辑为:当cache无数据时,就加载assets的数据
MediatorLiveData<DataResult<ContentBean>> cacheAndAssetsLiveData = new MediatorLiveData<>();
appExecutors.diskIO().execute(cacheLiveDataTask); // diskIO线程池里,执行缓存加载
cacheAndAssetsLiveData.addSource(cacheLiveDataTask.getLiveData(), cacheDataResult -> {
switch (cacheDataResult.status) {
case SUCCESS:
case LOADING:
cacheAndAssetsLiveData.setValue(cacheDataResult);
break;
case ERROR: // 如果加载出错,加载assets内置数据
cacheAndAssetsLiveData.removeSource(cacheLiveDataTask.getLiveData());
appExecutors.diskIO().execute(assetsLiveDataTask);
cacheAndAssetsLiveData.addSource(assetsLiveDataTask.getLiveData(), new Observer<DataResult<ContentBean>>() {
@Override
public void onChanged(DataResult<ContentBean> assetsDataResult) {
cacheAndAssetsLiveData.setValue(assetsDataResult);
}
});
break;
}
});

// 监听缓存数据,注意:返回的状态都是 加载中...
result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.loading(dataResult.data)));

// networkIO线程池里,执行网络加载
appExecutors.networkIO().execute(networkLiveDataTask);
// 逻辑:
// 1. 网络请求成功,就不再监听缓存数据了,不管缓存数据是否加载成功,都直接使用网络数据
// 2. 网络请求失败,就再监听级存数据,可能存在两种情况:
// 1. 缓存已经加载成功,已经回调首页的Activity了,但状态是Loading,会马上重新回调Activity,但状态为Error,数据一样
// 2. 缓存还没有加载成功,则等缓存加载成功后,直接回调Activity,但状态为Error,数据为缓存数据
result.addSource(networkLiveDataTask.getLiveData(), networkDataResult -> {
switch (networkDataResult.status) {
case SUCCESS:
result.removeSource(cacheAndAssetsLiveData);
result.removeSource(networkLiveDataTask.getLiveData());

// 同步更新缓存数据
appExecutors.diskIO().execute(mContentDataRepository.saveContentDataToCache(networkDataResult.data));
result.setValue(DataResult.success(networkDataResult.data));
break;
case ERROR:
result.removeSource(cacheAndAssetsLiveData);
result.removeSource(networkLiveDataTask.getLiveData());

result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.error(networkDataResult.message, dataResult.data)));
break;
case LOADING:
// 不加调状态,因为已经更新Loading状态了
break;
}
});

return result;
}

public static class RequestValues implements UseCase.RequestValues {
private final String cityDir;

public RequestValues(String cityDir) {
this.cityDir = cityDir;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RequestValues that = (RequestValues) o;
return Objects.equals(cityDir, that.cityDir);
}

@Override
public int hashCode() {
return Objects.hash(cityDir);
}

@Override
public String toString() {
return "RequestValues{" +
"cityDir='" + cityDir + '\'' +
'}';
}
}
}

VM层

HomePageViewModel:

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
public class HomePageViewModel extends ViewModel {

private final GetContentDataUseCase getContentDataUseCase;

private final MutableLiveData<String> cityDirLiveData = new MutableLiveData<>();
// 注意点:此处应该只返回LiveData,而不应该返回MutableLiveData类型
public LiveData<String> getCityDirLiveData(){
return cityDirLiveData;
}

public void retryCityDir(){
String cityDir = cityDirLiveData.getValue();
if(TextUtils.isEmpty(cityDir)) {
return ;
}
cityDirLiveData.setValue(cityDir);
}

public void setCityDir(String cityDir) {
if (TextUtils.isEmpty(cityDir) || cityDir.equals(cityDirLiveData.getValue())) {
return;
}

cityDirLiveData.setValue(cityDir);
}

/**
* 实际的子类为MediatorLiveData,监听了cityDir变化后,获取的contentData
*/
public final LiveData<DataResult<ContentBean>> contentData = Transformations.switchMap(cityDirLiveData, new Function<String, LiveData<DataResult<ContentBean>>>() {
MutableLiveData<DataResult<ContentBean>> mSource = new MutableLiveData<>();
@Override
public LiveData<DataResult<ContentBean>> apply(String cityDir) {
return getContentDataUseCase.execute(new GetContentDataUseCase.RequestValues(cityDir));
}
});

public HomePageViewModel(GetContentDataUseCase getContentDataUseCase) {
this.getContentDataUseCase = getContentDataUseCase;
}
}

单元测试

对 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@RunWith(JUnit4.class)
public class HomePageViewModelTest {

@Rule
@JvmField
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();

private final GetContentDataUseCase getContentDataUseCase = mock(GetContentDataUseCase.class);
private final HomePageViewModel homePageViewModel = new HomePageViewModel(getContentDataUseCase);

@Test
public void testNull(){
assertThat(homePageViewModel.contentData, notNullValue());
verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class));
}

@Test
public void dontFetchWithoutObservers(){
homePageViewModel.setCityDir("bj");
verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class));
}

@Test
public void fetchWhenObserved(){
homePageViewModel.setCityDir("bj");
homePageViewModel.contentData.observeForever(mock(Observer.class));
verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj"));
}

@Test
public void changeWhileObserved(){
homePageViewModel.contentData.observeForever(mock(Observer.class));

homePageViewModel.setCityDir("bj");
homePageViewModel.setCityDir("shanghai");

verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj"));
verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("shanghai"));
}

@Test
public void retry(){
homePageViewModel.retryCityDir();
verifyNoMoreInteractions(getContentDataUseCase);

homePageViewModel.setCityDir("bj");
verifyNoMoreInteractions(getContentDataUseCase);

homePageViewModel.contentData.observeForever(mock(Observer.class));
verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj"));

reset(getContentDataUseCase);
homePageViewModel.retryCityDir();
verify(getContentDataUseCase).execute(new GetContentDataUseCase.RequestValues("bj"));
}

@Test
public void blankCityDir(){
homePageViewModel.setCityDir("");
homePageViewModel.contentData.observeForever(mock(Observer.class));
verifyNoMoreInteractions(getContentDataUseCase);
}
}

对 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@RunWith(JUnit4.class)
public class GetContentDataUseCaseTest {

@Rule
@JvmField
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();

private MutableLiveData<DataResult<ContentBean>> assetsLiveData = new MutableLiveData<>();
private MutableLiveData<DataResult<ContentBean>> cacheLiveData = new MutableLiveData<>();
private MutableLiveData<DataResult<ContentBean>> netLiveData = new MutableLiveData<>();

private LiveDataTask<ContentBean> assetsTask = Mockito.mock(LiveDataTask.class);
private LiveDataTask<ContentBean> cacheTask = Mockito.mock(LiveDataTask.class);
private LiveDataTask<ContentBean> netTask = Mockito.mock(LiveDataTask.class);

private ContentDataRepository contentDataRepository;

private GetContentDataUseCase getContentDataUseCase;

@Before
public void init() {
// mock repository实现
contentDataRepository = Mockito.mock(ContentDataRepository.class);

// mock asset实现
Mockito.when(assetsTask.getLiveData()).thenReturn(assetsLiveData);
Mockito.when(contentDataRepository.contentDataFromAssets(Mockito.anyString())).thenReturn(assetsTask);

// mock cache实现
Mockito.when(cacheTask.getLiveData()).thenReturn(cacheLiveData);
Mockito.when(contentDataRepository.contentDataFromCache(Mockito.anyString())).thenReturn(cacheTask);

// mock net实现
Mockito.when(netTask.getLiveData()).thenReturn(netLiveData);
Mockito.when(contentDataRepository.contentDataFromNet(Mockito.anyString())).thenReturn(netTask);

getContentDataUseCase = new GetContentDataUseCase(
new InstantAppExecutors(),
contentDataRepository
);
}

@Test
public void testBegin(){
getContentDataUseCase.execute(new GetContentDataUseCase.RequestValues("bj"));
verify(cacheTask).run();
verify(netTask).run();
verifyNoMoreInteractions(assetsTask);
}

@Test
public void dontHaveCache(){
Observer<DataResult<ContentBean>> observer = mock(Observer.class);

getContentDataUseCase
.execute(new GetContentDataUseCase.RequestValues("bj"))
.observeForever(observer);

verify(observer).onChanged(DataResult.loading(null));

// 模拟 本地缓存加载失败
cacheLiveData.postValue(DataResult.error("not have data", null));
verify(assetsTask).run();

// 模拟 assets缓存的成功的情况
ContentBean assetsContentBean = mock(ContentBean.class);
assetsLiveData.postValue(DataResult.success(assetsContentBean));
verify(observer).onChanged(DataResult.loading(assetsContentBean));

// 模拟 net缓存请求成功的情况
ContentBean netContentBean = mock(ContentBean.class);
netLiveData.postValue(DataResult.success(netContentBean));
verify(observer).onChanged(DataResult.success(netContentBean));
}

@Test
public void haveCache(){
Observer<DataResult<ContentBean>> observer = mock(Observer.class);

getContentDataUseCase
.execute(new GetContentDataUseCase.RequestValues("bj"))
.observeForever(observer);

verify(observer).onChanged(DataResult.loading(null));

// 模拟 本地缓存加载成功
ContentBean cacheContentBean = mock(ContentBean.class);
cacheLiveData.postValue(DataResult.success(cacheContentBean));
verify(observer).onChanged(DataResult.loading(cacheContentBean));
verify(assetsTask, never()).run();

// 模拟 net缓存请求成功的情况
ContentBean netContentBean = mock(ContentBean.class);
netLiveData.postValue(DataResult.success(netContentBean));
verify(contentDataRepository).saveContentDataToCache(netContentBean); // 验证缓存方法是否执行
verify(observer).onChanged(DataResult.success(netContentBean));
}

@Test
public void netError(){
Observer<DataResult<ContentBean>> observer = mock(Observer.class);

getContentDataUseCase
.execute(new GetContentDataUseCase.RequestValues("bj"))
.observeForever(observer);

verify(observer).onChanged(DataResult.loading(null));

// 模拟 本地缓存加载成功
ContentBean cacheContentBean = mock(ContentBean.class);
cacheLiveData.postValue(DataResult.success(cacheContentBean));
verify(observer).onChanged(DataResult.loading(cacheContentBean));
verify(assetsTask, never()).run();

// 模拟 net请求失败的情况
netLiveData.postValue(DataResult.error("net error", null));
verify(observer).onChanged(DataResult.error("net error", cacheContentBean));
}

@Test
public void netErrorButQuick(){
Observer<DataResult<ContentBean>> observer = mock(Observer.class);

getContentDataUseCase
.execute(new GetContentDataUseCase.RequestValues("bj"))
.observeForever(observer);

verify(observer).onChanged(DataResult.loading(null));

// 模拟 net缓存失败的情况
netLiveData.postValue(DataResult.error("net error", null));
verify(observer, never()).onChanged(DataResult.error("net error", any(ContentBean.class)));

// 模拟 本地缓存加载成功
ContentBean cacheContentBean = mock(ContentBean.class);
cacheLiveData.postValue(DataResult.success(cacheContentBean));
verify(observer).onChanged(DataResult.error("net error", cacheContentBean));
}

@Test
public void netSuccessButQuick(){
Observer<DataResult<ContentBean>> observer = mock(Observer.class);

getContentDataUseCase
.execute(new GetContentDataUseCase.RequestValues("bj"))
.observeForever(observer);

verify(observer).onChanged(DataResult.loading(null));

// 模拟 net缓存请求成功的情况
ContentBean netContentBean = mock(ContentBean.class);
netLiveData.postValue(DataResult.success(netContentBean));
verify(observer).onChanged(DataResult.success(netContentBean));

// 模拟 本地缓存加载成功
ContentBean cacheContentBean = mock(ContentBean.class);
cacheLiveData.postValue(DataResult.success(cacheContentBean));
verify(observer, never()).onChanged(DataResult.loading(cacheContentBean));
}

}

参考

  1. 架构设计的历史·MVC·MVP·MVVM
  2. https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/
  3. NetworkBoundResourceTest.kt
  4. Repository图
  5. 各种框架代码
感谢您的阅读,本文由 刘阳 版权所有。如若转载,请注明出处:刘阳(https://handsomeliuyang.github.io/2021/04/26/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/
责任链模式两种典型实现
基于git和markdown的文档平台