在 Android 架构系列的最后部分 ,我们将 Clean Architecture 调整到 Android 平台。我们将 Android 和真实世界从业务逻辑中分离,令利益相关者满意,使一切都容易测试。
理论很棒,但是当我们创建一个新 Android 项目时,该从哪开始呢?让我们用整洁代码弄脏我们的手,把空白画布变成一个架构。
基础
让我们先做一些基础工作 —— 创建模块并建立依赖关系,使其与依赖规则保持一致。
这些将是我们的模块,从抽象到具体:
1. domain
实体、用例、仓库接口和设备接口放入 domain 模块。
理想情况下,实体和业务逻辑应该是平台无关的。为了安全起见,为了防止我们在这里放置一些 Android 的东西,我们将使它成为一个纯 java 模块。
2. data
data 模块应当持有与数据持久化和操作相关的所有内容。在这里我们可以找到 DAO、ORM、SharedPreferences、网络相关的比如 Retrofit Service 或类似的东西。
3. device
device 模块应该拥有与 Android 相关的所有东西(除了数据持久化 和 UI)。例如 ConnectivityManager, NotificationManager 和 misc 传感器的封装类。
我们将使 data 和 device 模块成为 Android 模块,因为它们必须知道 Android,不能是纯的 java。
4. 最容易的部分,app 模块(UI 模块)
当你创建项目时,该模块已经由 Android Studio 为你创建好了。
在这里,你可以放置与 Android UI 相关的类,譬如 Presenter,Controller,ViewModel,Adapter 以及 View。
依赖
依赖规则定义了具体模块依赖于抽象模块。
你可能会记得本系列的第三部分,UI(app),DB-API(data)以及设备(device)的东西都在外层。这意味着它们在同一抽象级别。那么我们该如何将它们串在一起呢?
理想的情况下,这些模块仅依赖于领域(domain)模块。在这种情况下,依赖关系看起来有点像一颗星:
但是这里我们涉及 Android 所以事情就不那么完美了。因为我们需要创建对象依赖图以及初始化一些东西,模块有时依赖另一个模块而不是领域模块。
例如,我们需要在 app 模块创建依赖注入的对象依赖图,这就迫使 app 模块需要知道其余所有模块。
我们调整后的依赖关系图:
砖,更多的砖
终于,是时候写些代码了。为了容易演示,我们将以 RSS 阅读器 app 为例。我们的用户应该能够管理他们的 RSS Feed 订阅,从 Feed 中获取文章并阅读它们。
领域
让我们从领域层开始,创建我们的核心业务模型和逻辑。
我们的业务模型非常简单:
- Feed - 持有 RSS Feed 相关数据比如 url、缩略 URL、标题和描述
- Article - 持有文章相关数据比如文章标题、url 和发表时间
至于我们的逻辑,我们将使用 UseCase 也就是交互器。它们将简单类中的小部分业务逻辑封装起来。它们都会实现一般的 UseCase 协议:
public interface UseCase<P> {
interface Callback {
void onSuccess();
void onError(Throwable throwable);
}
void execute(P parameter, Callback callback);
}
复制代码
当用户打开我们的 app 要做的第一件事情是添加一个新的 RSS Feed 订阅。所以从我们的交互器开始,我们创建
AddNewFeedUseCase
,以及处理 feed 添加和验证逻辑的辅助类。
AddNewFeedUseCase
使用
FeedValidator
来检查 feed URL 的有效性,我们还将创建
FeedRepository
协议,它为我们的业务逻辑提供基础的增删改查能力来管理 feed 数据:
public interface FeedRepository {
int createNewFeed(String feedUrl);
List<Feed> getUserFeeds();
List<Article> getFeedArticles(int feedId);
boolean deleteFeed(int feedId);
}
复制代码
注意我们在领域层的 命名 是如何清晰地说明我们的 app 是做什么的。
把所有东西放在一起,我们的
AddNewFeedUseCase
看起来像这样:
public final class AddNewFeedUseCase implements UseCase<String> {
private final FeedValidator feedValidator;
private final FeedRepository feedRepository;
@Override
public void execute(final String feedUrl, final Callback callback) {
if (feedValidator.isValid(feedUrl)) {
onValidFeedUrl(feedUrl, callback);
} else {
callback.onError(new InvalidFeedUrlException());
}
}
private void onValidFeedUrl(final String feedUrl, final Callback callback) {
try {
feedRepository.createNewFeed(feedUrl);
callback.onSuccess();
} catch (final Throwable throwable) {
callback.onError(throwable);
}
}
}
复制代码
为了简洁起见,省略了构造函数。
现在,你可能会困惑,为什么我们的用例和回调是一个接口?
为了更好地演示我们下一个问题,让我们来研究研究
GetFeedArticlesUseCase
获得 feedId -> 通过 FeedRepository 抓取 feed 文章 -> 返回 feed 文章列表
这是数据流问题,用例位于表现层和数据层之间,我们怎样建立起层和层之间的通信?记得那些输入和输出端口吗?
我们的用例必须实现输入端口(interface)。Presenter 调用用例的方法,数据流向用例(feedId)。用例将 feedId 转换成 feed 文章列表,并希望将其返回给表现层。它拥有指向输出端口(Callback)的引用,因为输出端口是定义在同一层的,所以它调用了输出端口的一个方法。因此数据将发送到输出端口 —— Presenter。
我们稍微调整一下 UseCase 协议:
public interface UseCase<P, R> {
interface Callback<R> {
void onSuccess(R return);
void onError(Throwable throwable);
}
void execute(P parameter, Callback<R> callback);
}
public interface CompletableUseCase<P> {
interface Callback {
void onSuccess();
void onError(Throwable throwable);
}
void execute(P parameter, Callback callback);
}
复制代码
UseCase 接口是输入端口,而 Callback 接口是输出端口。
GetFeedArticlesUseCase
实现如下:
class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> {
private final FeedRepository feedRepository;
@Override
public void execute(final Integer feedId, final Callback<List<Article>> callback) {
try {
callback.onSuccess(feedRepository.getFeedArticles(feedId));
} catch (final Throwable throwable) {
callback.onError(throwable);
}
}
}
复制代码
最后一件领域层需要注意的事情是,交互器只应该包含业务逻辑。在这样做时,它们可以使用 Repository,组合其它交互器,使用类似我们例子中
FeedValidator
这样的公共设施类。
UI
很好,我们可以抓取文章,现在让我们向用户展示它们。
我们的 View 有一个简单的协议:
interface View {
void showArticles(List<ArticleViewModel> feedArticles);
void showErrorMessage();
void showLoadingIndicator();
}
复制代码
此 View 的 Presenter 的表现逻辑非常简单。它抓取文章,转换成视图模型传递给 View,简单吧,对吗?
简单的 Presenter 是 Clean Architecture 和 表现 —— 业务逻辑分离的另一个伟大成就。
这是我们的
FeedArticlesPresenter
:
class FeedArticlesPresenter implements UseCase.Callback<List<Article>> {
private final GetFeedArticlesUseCase getFeedArticlesUseCase;
private final ViewModeMapper viewModelMapper;
public void fetchFeedItems(final int feedId) {
getFeedArticlesUseCase.execute(feedId, this);
}
@Override
public void onSuccess(final List<Article> articles) {
getView().showArticles(viewModelMapper.mapArticlesToViewModels(articles));
}
@Override
public void onError(final Throwable throwable) {
getView().showErrorMessage();
}
}
复制代码
注意
FeedArticlesPresenter
实现了 Callback 接口,并将自身传递给用例,它实际上是用例的输出端口,并以这种方式关闭数据流。这是我们前面提到过的数据流的具体例子,我们可以在流程图上调整标签来匹配这个例子:
我们的参数 P 是整数 feedId,返回类型 R 是文章列表。
你不一定必须使用 Presenter 来处理表现逻辑,我们可以说,Clean Architecture 是“前端”无关的 —— 这意味着你可以使用 MVP,MVC,MVVM 或其他任何东西。