在Android应用中使用Clean架构
自从开始开发安卓应用,我一直感觉我可以做得更好。我看过不少烂代码,其中当然有我写的。安卓系统的复杂性加上烂代码势必酿成灾祸,所以从错误中成长就很重要。我Google了如何更好地开发应用,发现了这个叫做Clean架构的东西。于是我尝试将它应用于安卓开发,根据我在类似项目中的经验做了一些改善,写出了这篇我觉得较为实用、值得分享的文章。
我会在这篇文章中手把手教你在Android应用中使用Clean架构。我最近一直用这种方式优雅地编写应用。
什么是Clean架构?
有许多文章已经很好地回答了这个问题。我在这里讲一讲Clean架构的核心概念。
一般来说,在Clean架构中,代码被分层成洋葱形,层层包裹,其中有一个依赖性规则:内层不能依赖外层,即内层不知道有关外层的任何事情,所以这个架构是向内依赖的。看个图感受一下:
图片由Bob大叔提供
Clean架构可以使你的代码有如下特性:
- 独立于架构
- 易于测试
- 独立于UI
- 独立于数据库
- 独立于任何外部类库
我将通过下面的例子解释这些特性是怎么来的。如果你想深入了解Clean架构,不妨看这篇文章和这个视频
Clean在Android中如何表现
一般来说,一个应用可以有任意数目的层,但除非你的应用到处是企业级功能逻辑,一般需要这三层:
- 外层:实现层
- 中层:接口适配层
- 内层:逻辑层
接口实现层是体现架构细节的地方。实现架构的代码是所有不用来解决问题的代码,这包括所有与安卓相关的东西,比如创建Activity和Fragment,发送Intent以及其他联网与数据库的架构相关的代码。
添加接口适配层的目的就是桥接逻辑层和架构层的代码。
最重要的是逻辑层,这里包含了真正解决问题的代码。这一层不包含任何实现架构的代码,不用模拟器也应能运行这里的代码。这样一来你的逻辑代码就有了易于测试、开发和维护的优点。这就是Clean架构的一个主要的好处。
每一个位于核心层外部的层都应能将外部模型转成可以被内层处理的内部模型。内层不能持有属于外层的模型类的引用。这也是由于刚才说的依赖性规则,这样内外层可以很好地分离。
为什么要进行模型转换呢?举个例子,当逻辑层的模型不能直接很优雅地展现给用户,或是需要同时展示多个逻辑层的模型时,最好创建一个ViewModel类来更好的进行UI展示。这样一来,你就需要一个属于外层的Converter类来将逻辑层模型转换成合适的ViewModel。
再举一个例子:你从外部数据库层获得了ContentProvider的Cursor对象,外层首先要将这个对象转换成内层模型,再将它传给内层处理。
在文章的最后我还提供了一些学习资源。我们已经知道了Clean架构的基本原则,现在我们来实践一下。我会在下一部分中使用Clean架构构建一个示例功能。
如何开始写Clean应用?
我已经写好了一个样板项目,里面把准备工作做好了。这相当于是一个Clean的底层包,可以直接在它的基础上进行开发。请随意下载、修改。项目包:Android Clean Boilerplate
开始写用例
这一部分会详细说明如何用在样例项目的基础之上以Clean方式进行开发。首先让我们看一下应用的结构,当这只是我的习惯,不需要完全按这个进行。
结构
一般来说一个安卓应用的结构如下:
- 外层项目包:UI,Storage,Network等等。
- 中层项目包:Presenter,Converter。
- 内层项目包:Interactor,Model,Repository,Executor。
看不懂不要紧,下面有具体解释。
外层
外层体现了框架的细节。
UI – 包括所有的Activity,Fragment,Adapter和其他UI相关的Android代码。
Storage – 用于让交互类获取和存储数据的接口实现类,包含了数据库相关的代码。包括了如ContentProvider或DBFlow等组件。
Network – 网络操作。
中层
桥接实现代码与逻辑代码的Glue Code。
Presenter – presenter处理UI事件,如单击事件,通常包含内层Interactor的回调方法。
Converter – 负责将内外层的模型互相转换。
内层
内层包含了最高级的代码,里面都是POJO类,这一层的类和对象不知道外层的任何信息,且应能在任何JVM下运行。
Interactor – Interactor中包含了解决问题的逻辑代码。这里的代码在后台执行,并通过回调方法向外层传递事件。在其他项目中这个模块被称为用例Use Case。一个项目中可能有很多小Interactor,这符合单一职责原则,而且这样更容易让人接受。
Model – 在业务逻辑代码中操作的业务模型。
Repository – 包含接口让外层类实现,如操作数据库的类等。Interactor用这些接口的实现类来读取和存储数据。这也叫资源库模式Repository Pattern。
Executor – 通过Worker Thread Executor让Interactor在后台执行。一般不需要修改这个包里的代码。
以下是例子
在这个简单例子中,我们的use case是在应用启动时读取数据库中的欢迎语句并展示。下面演示如何编写代码包让use case运行起来。
- presentation包
- storage包
- domain包
前两个包属于外层,最后一个包属于内层(核心层)。
presentation包负责将信息展示在屏幕上,而且包含整个MVP栈,即同时包含UI和presenter这两个属于不同层的组件。下面上码。
写一个内层的Interactor
你可以从任何一层开始编写,我建议从内层的逻辑代码写起。因为逻辑代码写好之后可以测试,不需要activity也可以正常运行。
所以我们先写一个Interactor,这个Interactor包含了处理业务逻辑的代码。**所有的Interactor都应该在后台运行,而不应影响UI展示。**我在这里先编写一个WelcomingInteractor。
public interface WelcomingInteractor extends Interactor {
interface Callback {
void onMessageRetrieved(String message);
void onRetrievalFailed(String error);
}
}
Callback负责与主线程的UI组件联通。将它放在WelcomingInteractor中可以避免给所有Callback接口起不同的名字而又能将它们有效区分。而后我们要实现获取消息的逻辑。现在已经有一个接口MessageRepository用于获取数据:
public interface MessageRepository {
String getWelcomeMessage();
}
现在我们可以用业务逻辑代码来实现Interactor接口了。注意要实现AbstractInteractor接口,这样代码就会在后台执行了。
public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {
...
private void notifyError() {
mMainThread.post(new Runnable() {
@Override
public void run() {
mCallback.onRetrievalFailed("Nothing to welcome you with :(");
}
});
}
private void postMessage(final String msg) {
mMainThread.post(new Runnable() {
@Override
public void run() {
mCallback.onMessageRetrieved(msg);
}
});
}
@Override
public void run() {
// 获取消息
final String message = mMessageRepository.getWelcomeMessage();
// 检查是否获取失败
if (message == null || message.length() == 0) {
// 在主线程中通知错误
notifyError();
return;
}
// 已成功获取消息,通知UI
postMessage(message);
}
}
这段代码获取了数据,并向UI层发送数据或报错。这里通过Callback向UI发送信息,这个Callback扮演的是presenter的角色。这段代码是逻辑的核心,其他代码都是依赖框架的。看一下这个类的引用:
import com.kodelabs.boilerplate.domain.executor.Executor;
import com.kodelabs.boilerplate.domain.executor.MainThread;
import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor;
import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor;
import com.kodelabs.boilerplate.domain.repository.MessageRepository;
可以看到,没有和Android相关的类库,这就是Clean架构的好处。还有就是写逻辑代码时不需要关心UI或数据库,只需要调用外层实现的Callback的回调方法。
测试Interactor
现在不需要模拟器也可以运行这段代码了,我们编写一个JUnit测试来确保这段代码运行正常。
@Test
public void testWelcomeMessageFound() throws Exception {
String msg = "Welcome, friend!";
when(mMessageRepository.getWelcomeMessage()).thenReturn(msg);
WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(
mExecutor,
mMainThread,
mMockedCallback,
mMessageRepository
);
interactor.run();
Mockito.verify(mMessageRepository).getWelcomeMessage();
Mockito.verifyNoMoreInteractions(mMessageRepository);
Mockito.verify(mMockedCallback).onMessageRetrieved(msg);
}
重复一遍,Interactor根本不知道它在Android环境下运行。
编写presentation层
Presentation层在Clean架构中属于外层的范围,它依赖于框架,包含了UI展示的代码。我们用MainActivity类在应用启动时展示欢迎信息。
首先编写Presenter和View的接口。View只需要展示欢迎信息。
public interface MainPresenter extends BasePresenter {
interface View extends BaseView {
void displayWelcomeMessage(String msg);
}
}
那怎么在App启动时运行Interactor呢?所有和View无关的代码都写进Presenter类中。这样可以实现关注分离(Separation of Concerns)并能避免Activity过于复杂。这些代码包括和Interactor交互的代码。
在MainActivity中重写onResume()方法。
@Override
protected void onResume() {
super.onResume();
// 在活动resume时开始获取数据
mPresenter.resume();
}
所有的Presenter在继承BasePresenter时都要实现resume()方法。我们在MainPresenter的onResume()方法中启动Interactor。
@Override
public void resume() {
mView.showProgress();
// 初始化Interactor
WelcomingInteractor interactor = new WelcomingInteractorImpl(
mExecutor,
mMainThread,
this,
mMessageRepository
);
// 执行interactor
interactor.execute();
}
execute()方法会在后台线程中调用WelcomingInteractorImpl类的run()方法。run()方法的实现可以看上文写一个内层的Interactor部分。
你可能已经发现Interactor很像AsyncTask,都是提供所有需要的东西然后运行。那为什么不用AsyncTask呢?因为AsyncTask是Android的代码,需要模拟器来运行与测试。
在上面的代码中我们给Interactor传入了下列属性:
- ThreadExecutor对象:用于在后台线程运行Interactor。我喜欢将这个类设计成单例。这个类属于domain包,不需要在外层实现。
- MainThreadImpl对象:用于在主线程中执行Interactor的Runnable对象。在依赖框架的外层代码中我们可以访问主线程,所以这个类要在外层实现。
- 我们传入this是因为MainPresenter也是一个Callback对象,Interactor要通过Callback来更新UI。
- 我们传入实现了MessageRepository接口的WelcomMessageRepository对象让Interactor使用。下面会讲到WelcomMessageRepository。
为什么this也是Callback呢?因为MainActivity的MainPresenter实现了Callback接口:
public class MainPresenterImpl extends AbstractPresenter implements MainPresenter,
WelcomingInteractor.Callback {
我们就是这么监听Interactor的事件的。下面是MainPresenter的代码:
@Override
public void onMessageRetrieved(String message) {
mView.hideProgress();
mView.displayWelcomeMessage(message);
}
@Override
public void onRetrievalFailed(String error) {
mView.hideProgress();
onError(error);
}
在代码段中我们看到的View其实就是实现了MainPresenter.View接口的MainActivity:
public class MainActivity extends AppCompatActivity implements MainPresenter.View {
View用于展示消息:
@Override
public void displayWelcomeMessage(String msg) {
mWelcomeTextView.setText(msg);
}
Presentation层的东西就这么多了。
编写Storage层
repository中的接口就在storage层实现。所有与数据库相关的代码都在这里。资源库模式下数据的来源是不确定的,意思是逻辑代码不关心数据的来源,不论是数据库、服务器还是文件。
你可以用ContentProvider或DBFlow等ORM工具处理更复杂的数据。如果你需要从网络获取数据那你可以用Retrofit。如果你只需要基本的键值对存储那你可以用SharedPreferences。不管怎样,一定要选对工具。
这里我们的数据库不是真正的数据库,只是一个模拟了延迟的一个很简单的类。
public class WelcomeMessageRepository implements MessageRepository {
@Override
public String getWelcomeMessage() {
String msg = "Welcome, friend!";
// 模拟网络/数据库延迟
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return msg;
}
}
WelcomingInteractor可能以为延迟是网络或其他原因造成的,但它并不关心,它只需要数据提供者实现了MessageRepository接口。
总结
详细代码请看这个git repo。总结一下各个类的触发顺序:
MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity
控制流的顺序:
Outer — Mid — Core — Outer — Core — Mid — Outer
在一个use case中多次访问外层很正常。比如当你要显示、存储加访问网络,你的控制流会访问外层至少三次。