在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中多次访问外层很正常。比如当你要显示、存储加访问网络,你的控制流会访问外层至少三次。
在Android应用中使用Clean架构的更多相关文章
- 转:Android开发中的MVP架构(最后链接资源不错)
Android开发中的MVP架构 最近越来越多的人开始谈论架构.我周围的同事和工程师也是如此.尽管我还不是特别深入理解MVP和DDD,但是我们的新项目还是决定通过MVP来构建. 这篇文章是我通过研究和 ...
- 转: Android开发中的MVP架构详解(附加链接比较不错)
转: http://www.codeceo.com/article/android-mvp-artch.html 最近越来越多的人开始谈论架构.我周围的同事和工程师也是如此.尽管我还不是特别深入理解M ...
- 设计模式笔记之二:Android开发中的MVP架构(转)
写在前面,本博客来源于公众号文章:http://mp.weixin.qq.com/s?__biz=MzA3MDMyMjkzNg==&mid=402435540&idx=1&sn ...
- Android中的MVP架构初探
说来羞愧,MVP的架构模式已经在Android领域出现一两年了.可是到今天自己才開始Android领域中的MVP架构征程. 闲话不多说,開始吧. 一.架构演变概述 我记得我找第一份工作时,面试官问我& ...
- Android项目中如何用好构建神器Gradle?(转)
最近在忙团队并行开发的事情,主要是将各个团队的代码分库,一方面可以降低耦合,为后面模块插件化做铺垫,另一方面采用二进制编译,可以加快编译速度.分库遇到了一些问题,很多都要通过Gradle脚本解决,所以 ...
- Android开发中Eclispe相关问题及相应解决(持续更新)
1.Eclipse项目中的Android Private Libraries没有自动生成. 一般而言,在Android开发中,项目中引用到的jar包会放到项目目录中的libs中,引入库会放到Andro ...
- android项目中配置NDK自动编译生成so文件
1 下载ndk开发包 2 在android 项目中配置编译器(以HelloJni项目为例) 2.1 创建builer (a)Project->Properties->Builder ...
- 关于Android开发中的证书和密钥等问题
关于Android开发中的证书和密钥等问题 引言 除了Android发布应用签名时需要用到证书外,在进行google Map Api开发和Facebook SDK API开发等时都需要申请API Ke ...
- Android 客户端应用开发的架构
本文算是一篇漫谈,谈一谈关于android开发中工程初始化的时候如何在初期我们就能搭建一个好的架构.关于android架构,因为手机的限制,目前我觉得也确实没什么大谈特谈的,但是从开发的角度,看到整齐 ...
随机推荐
- linux源码阅读笔记 数组定义
在阅读linux源码的过程中遇到了下面的略显奇怪的结构体数组定义. static struct hd_struct{ long start_sect; long nr_sects; }hd[10]={ ...
- python中精确输出JSON浮点数的方法
有时需要在JSON中使用浮点数,比如价格.坐标等信息.但python中的浮点数相当不准确, 例如下面的代码: 复制代码代码如下: #!/usr/bin/env python import json a ...
- zoj 3232 It's not Floyd Algorithm(强联通分量,缩点)
题目 /******************************************************************/ 以下题解来自互联网:Juny的博客 思路核心:给你的闭包 ...
- KMP笔记√//找最大子串,前缀自匹配长度
假设s1里找s2,然后s2进去匹配假设在第三位失配那么说明前两位是匹配成功的 如果这时候将s2后移一位相当于将s2的第一位和s2的第二位比较,如果我们已知s1(1)≠s1(2)那么就可以直接后移两位 ...
- 【memcache缓存专题(1)】memcache的介绍与应用场景
简介 Memcached是一个高性能的分布式的内存对象缓存系统,目前全世界不少人使用这个缓存项目来构建自己大负载的网站,来分担数据库的压力,通过在内存里维护一个统一的巨大的hash表,它能够用来存储各 ...
- [Unity菜鸟] FBX模型动画提取
角色已经人形化(Humanoid)了,那它的动画可以用在其它的模型上了也就是可以共用一套模型动画了,但是你有没有发现那动画是和fbx模型绑在一起的,没关系你可以选中这几个动画文件按Contrl+D就可 ...
- Android:储存方式之SharePreferences
使用SharedPreferences保存数据,其实质是采用了xml文件存放数据, 存储位置:/data/data/<package name>/shared_prefs 写入: publ ...
- Lambda 表达式型的排序法
int[] arry = {3,9,5,7,64,51,35,94 }; foreach (int i in arry.OrderBy(i => i)) Console.WriteLine(i) ...
- Java NIO1
发现了一个很好的学习Java的外国网站,英语都是很简单的啦,看英语舒服些,关于NIO的系列就直接参照此网站了,而且是英语的! http://tutorials.jenkov.com/ Java NIO ...
- Spring与Hibernate整合
Spring与Struts2整合的目的: 让Spring管理Action Spring整合Hinernate的目的: --管理SessionFactory(单例的),数据源 --声明式事务管理 1.首 ...