Android架构:第四部分-在Android上应用Clean架构,实践(包含源代码)(译)

妖精的绣舞 提交于 2019-12-03 05:52:48

在Android Architecture系列的最后一部分,我们将Clean Architecture稍微调整到了Android平台。 我们将Android和现实世界从业务逻辑中分离出来,让满意的利益相关者满意,并让所有事情都可以轻松测试。

这个理论很好,但是当我们创建一个新的Android项目时,我们从哪里开始? 让我们用干净的代码弄脏我们的手,并将空白的画布变成一个架构。

基础

我们将首先奠定基础 - 创建模块并建立它们之间的依赖关系,以便与依赖规则保持一致。

这些将是我们的模块,从最抽象的一个到具体的实现:

1. domain

Entities, use cases, repositories interfaces, 和 device interfaces 进入 domain module。

理想情况下,实体和业务逻辑应该是平台不可知的。 为了安全起见,为了防止我们在这里放置一些Android的东西,我们将使它成为一个纯粹的Java模块。

2. data

数据模块应包含与数据持久性和操作相关的所有内容。 在这里,我们将找到DAO,ORM,SharedPreferences,网络相关的东西,例如Retrofit服务和类似的东西。

3. device

设备模块应该包含与Android相关的所有内容,而不是数据持久性和UI。 例如,ConnectivityManager,NotificationManager和misc传感器的包装类。

我们将使数据和设备模块都是Android模块,因为他们必须了解Android并且不能是纯Java。

4. The easiest part, app module (UI module)

创建项目时,Android模块已经为您创建了该模块。

在这里,您可以放置与Android UI相关的所有类,例如presenters,controllers,view models,adapters和views。

依赖

依赖规则定义具体模块依赖于更抽象的模块。

您可能还记得,从本系列的第三部分可以看出,UI(应用程序),DB-API(数据)和Device(设备)等东西都在外环中。 这意味着它们处于相同的抽象层次。 我们如何将它们连接在一起呢?

理想情况下,这些模块仅取决于域模块。 在这种情况下,依赖关系看起来有点像明星:

但是,我们在这里与Android打交道,事情并不完美。 因为我们需要创建对象图并初始化事物,所以模块有时依赖于domain以外的其他模块。

例如,我们正在app模块中创建用于依赖注入的对象图。 这迫使APP模块了解所有其他模块。

我们调整后的依赖关系图:

最后,是时候编写一些代码。 为了更容易,我们将以RSS Reader APP为例。 我们的用户应该能够管理他们的RSS提要订阅,从提要中获取文章并阅读它们。

Domain

让我们从domain层开始,创建我们的核心业务模型和逻辑。

我们的商业模式非常简单:

  • Feed - 持有RSS提要相关数据,如网址,缩略图网址,标题和说明
  • Article -保存文章相关数据,如文章标题,网址和发布日期

而对于我们的逻辑,我们将使用UseCases。 他们在简洁的类中封装了小部分业务逻辑。 他们都将实施通用的UseCase 契约类:

1
2
3
4
5
6
7
8
9
10
public interface UseCase<P> {

   interface Callback {

      void onSuccess();
      void onError(Throwable throwable);
    }

   void execute(P parameter, Callback callback);
 }

我们的用户在打开我们的应用时首先要做的就是添加一个新的RSS订阅。 因此,要开始使用我们的Use Case,我们将创建AddNewFeedUseCase及其助手来处理Feed的添加和验证逻辑。

AddNewFeedUseCase将使用FeedValidator来检查Feed URL的有效性,并且我们还将创建FeedRepository 契约类,这将为我们的业务逻辑提供一些基本的CRUD功能来管理供稿数据:

1
2
3
4
5
6
7
8
9
10
public interface FeedRepository {

    int createNewFeed(String feedUrl);

    List<Feed> getUserFeeds();

    List<Article> getFeedArticles(int feedId);

    boolean deleteFeed(int feedId);
}

请注意我们在Domain层的命名是如何清楚地传递我们的APP正在做什么的想法。

把所有东西放在一起,我们的AddNewFeedUseCase看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
       }
   }
}

ps:为简洁起见,省略构造函数。

现在,您可能想知道,为什么我们的use case以及我们的回调是一个接口?

为了更好地展示我们的下一个问题,让我们来研究GetFeedArticlesUseCase

它需要一个feedId - >通过FeedRespository获取提要文章 - >返回提要文章

这里是数据流问题,用例介于表示层和数据层之间。 我们如何建立层之间的沟通? 记住那些输入和输出端口?

我们的 Use Case必须实现输入端口(接口)。 Presenter在 Use Case上调用方法,数据流向 Use Case(feedId)。 Use Case映射feedId提供文章并希望将它们发送回表示层。 它有一个对输出端口(回调)的引用,因为输出端口是在同一层定义的,因此它调用了一个方法。 因此,数据发送到输出端口 - Presenter。

我们将稍微调整我们的UseCase契约类:

1
2
3
4
5
6
7
8
9
public interface UseCase<P, R> {

   interface Callback<R> {
       void onSuccess(R return);
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback<R> callback);
}
1
2
3
4
5
6
7
8
9
public interface CompletableUseCase<P> {

   interface Callback {
       void onSuccess();
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback callback);
}

UseCase接口是输入端口,Callback接口是输出端口。

GetFeedArticlesUseCase实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
       }
   }
 }

在Domain层中要注意的最后一件事是Interactors应该只包含业务逻辑。 在这样做的时候,他们可以使用存储库,结合其他交互器,并在我们的例子中使用一些实用工具对象,如FeedValidator。

UI

太棒了,我们可以获取文章,让我们现在将它们展示给用户。

我们的View有一个简单的契约类:

1
2
3
4
5
6
7
8
interface View {

   void showArticles(List<ArticleViewModel> feedArticles);
   
   void showErrorMessage();
   
   void showLoadingIndicator();
}

该视图的Presenter具有非常简单的显示逻辑。 它获取文章,将它们映射到view odels并传递到View,很简单,对吧?

简单的presenters是Clean架构和presentation - business逻辑分离的又一壮举。

这里是我们的FeedArticlesPresenter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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接口,并将其自身传递给use case,它实际上是use case的输出端口,并以这种方式关闭了数据流。 这是我们前面提到的数据流的具体示例,我们可以在流程图上调整标签以匹配此示例:

我们的参数P是feedId,返回类型R是文章列表。

您不必使用Presenter来处理显示逻辑,我们可以说Clean架构是“前端”不可知的 - 这意味着您可以使用MVP,MVC,MVVM或其他任何东西。

让我们在混合中抛出一些Rx

现在,如果你想知道为什么有这样的RxJava,我们将看看我们UseCase的反应式实现:

1
2
3
4
public interface UseCase<P, R> {

   Single<R> execute(P parameter);         
}
1
2
3
4
public interface CompletableUseCase<P> {

    Completable execute(P parameter);
 }

回调接口现在不见了,我们使用RxJava Single / Completable接口作为我们的输出端口。

Reactive GetFeedArticlesUseCase

1
2
3
4
5
6
7
8
9
class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> {

   private final FeedRepository feedRepository;
   
   @Override
   public Single<List<Article>> execute(final Integer feedId) {
       return feedRepository.getFeedArticles(feedId);
   }
}

Reactive FeedArticlePresenter 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FeedArticlesPresenter {
 
   private final GetFeedArticlesUseCase getFeedArticlesUseCase;
   private final ViewModeMapper viewModelMapper;
 
   public void fetchFeedItems(final int feedId) {
       getFeedItemsUseCase.execute(feedId)
                  .map(feedViewModeMapper::mapFeedItemsToViewModels)
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(this::onSuccess, this::onError);
   }
 
   private void onSuccess(final List articleViewModels) {
      getView().showArticles(articleViewModels);
   }
 
   private void onError(final Throwable throwable) {
      getView().showErrorMessage();
   }
}

虽然它有点隐藏,但同样的数据流反演原理仍然存在,因为没有RxJava Presenters实现回调,并且RxJava订阅者也包含在外层 - 在Presenter的某处。

Data 和 Device

Data和Device包含业务逻辑不关心的所有实现细节。 它只关心契约类,允许您轻松测试它并在不触及业务逻辑的情况下交换实施。

在这里,您可以使用自己喜欢的ORM或DAO在本地存储数据,并使用网络服务从网络获取数据。 我们将实现FeedService来获取文章,并使用FeedDao将文章数据存储在设备上。

每个数据源(网络和本地存储)都将有自己的模型可供使用。

在我们的例子中,它们是ApiFeed - ApiArticleDbFeed - DbArticle

FeedRepository的具体实现也可以在Data模块中找到。

Device模块将持有作为NotificationManager类的包装的通知合同的实现。 我们也许可以使用业务逻辑中的通知来在用户可能感兴趣并推动参与的新文章发布时向用户显示通知。

Models, models everywhere.

您可能已经注意到我们提到的不仅仅是实体或业务模型,还有更多的模型。

实际上,我们也有db模型,API模型,View模型,当然还有业务模型。

对于每个图层来说,都有一个很好的实践,可以使用它自己的模型,因此具体的细节(如View)不依赖于较低层实现的具体细节。 这样,例如,如果您决定从一个ORM更改为另一个,则不必分解不相关的代码。

为了实现这一点,有必要在每个图层中使用对象映射器。 在示例中,我们使用ViewModelMapper将Domain 里的Article模型映射到ArticleViewModel

总结

遵循这些准则,我们创建了一个强大且多功能的架构。 起初,它可能看起来像很多代码,它有点像,但请记住,我们正在构建我们的架构以适应未来的变化和功能。 如果你做得对,未来你会感恩。

在下一部分中,我们将会介绍这个架构中最重要的部分,可测试性以及如何测试它。

那么,在此期间,您最感兴趣的是架构实现的哪一部分?

这是Android Architecture系列的一部分。 检查我们的其他部分:

Part 4: Applying Clean Architecture on Android (Hands-on)

Part 3: Applying Clean Architecture on Android

Part 2: The Clean Architecture

Part 1: every new beginning is hard

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!