用Kotlin构建反应组件系统



大家好! 我叫Anatoly Varivonchik,我是Badoo的一名Android开发人员。 今天,我将与您分享我的同事Zsolt Kocsi关于MVI实施的文章第二部分的翻译,我们在开发过程中每天都会使用它。 第一部分在这里

我们想要什么以及如何做


在本文的第一部分中,我们介绍了Features它们是可重用的MVICore的核心元素。 它们可以具有最简单的结构并仅包含一个Reducer ,也可以成为用于管理异步任务,事件等的功能齐全的工具。

每个功能都是可跟踪的-有机会订阅其状态的更改并接收有关它的通知。 在这种情况下,可以将功能订阅到输入源。 这是有道理的,因为在代码库中包含了Rx之后,我们已经在各个级别上拥有了许多可观察的对象和订阅。

与反应性组件数量的增加有关的是时候反思我们所拥有的以及是否有可能使系统变得更好。

我们必须回答三个问题:

  1. 添加新的反应成分时应使用哪些元素?
  2. 管理订阅的最简单方法是什么?
  3. 是否有可能忽略生命周期管理/需要清除订阅以避免内存泄漏? 换句话说,我们可以将组件绑定与订阅管理分开吗?

在本文的这一部分中,我们将研究使用反应组件构建系统的基本知识和好处,并了解Kotlin如何提供帮助。

主要要素


当我们开始设计功能部件的设计和标准化时,我们已经尝试了许多不同的方法,并决定将功能部件作为反应组件的形式。 首先,我们专注于主要界面。 首先,我们需要确定输入和输出数据的类型。

我们推理如下:

  • 让我们不要重新发明轮子-让我们看看已经存在哪些接口。
  • 由于我们已经在使用RxJava库,因此引用其基本接口很有意义。
  • 接口的数量应最小化。

结果,我们决定将ObservableSource <T>用于输出,将Consumer <T>用于输入。 你问为什么不可以观察/观察者Observable是您需要从其继承的抽象类,而ObservableSource是您实现的接口,该接口完全满足实现反应式协议的需求。

package io.reactivex; import io.reactivex.annotations.*; /** * Represents a basic, non-backpressured {@link Observable} source base interface, * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableSource instance. * @param observer the Observer, not null * @throws NullPointerException if {@code observer} is null */ void subscribe(@NonNull Observer<? super T> observer); } 

Observer是第一个想到的接口,它实现四个方法:onSubscribe,onNext,onError和onComplete。 为了尽可能简化协议,我们首选Consumer <T> ,它使用一种方法接受新元素。 如果选择Observer ,则其余方法通常是多余的或工作方式不同(例如,我们希望将错误作为状态的一部分而不是异常,并且肯定不会中断流)。

 /** * A functional interface (callback) that accepts a single value. * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; } 

因此,我们有两个接口,每个接口包含一个方法。 现在我们可以通过将Consumer <T>签名到ObservableSource <T>来绑定它们。 后者仅接受Observer <T>的实例,但是我们可以将其包装在Observable <T>中 ,该Observable <T>订阅了Consumer <T>

 val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input) 

(幸运的是,如果输出已经是Observable <T> ,则.wrap(输出)函数不会创建新对象。)

您可能还记得本文第一部分中的Feature组件使用了Wish类型的输入数据(对应于Model-View-Intent中的Intent)和State类型的输出,因此它可以位于包的两侧:

 // Wishes -> Feature val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish) val feature: Consumer<Wish> = SomeFeature() val disposable = Observable.wrap(wishes).subscribe(feature) // Feature -> State consumer val feature: ObservableSource<State> = SomeFeature() val logger: Consumer<State> = Consumer { System.out.println(it) } val disposable = Observable.wrap(feature).subscribe(logger) 

消费者生产者的这种链接已经看起来很简单,但是有一种甚至更简单的方法,您无需手动创建订阅或取消订阅。

介绍活页夹

类固醇结合


MVICore包含一个名为Binder的类,该类提供了用于管理Rx订阅的简单API,并具有许多很酷的功能。

为什么需要它?

  • 通过将输入订阅到周末来创建绑定。
  • 可以在生命周期结束时退订(当它是一个抽象概念,与Android无关时)。
  • 奖励: 活页夹允许您添加中间对象,例如,用于日志记录或时间旅行调试。

您可以按以下方式重写上面的示例,而不是手动签名:

 val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger) 

感谢Kotlin,一切看起来都很简单。

如果输入和输出的类型相同,这些示例将起作用。 但是,如果不是这样呢? 通过实现扩展功能,我们可以使转换自动进行:

 val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer) 

注意语法:它的读法几乎就像是普通的句子(这是我喜欢Kotlin的另一个原因)。 但是Binder不仅被用作语法糖,它对我们解决生命周期问题也很有用。

创建活页夹


创建实例看起来并不容易:

 val binder = Binder() 

但是在这种情况下,您需要手动取消订阅,并且每当需要删除订阅时都必须调用binder.dispose() 。 还有另一种方法:将生命周期实例注入构造函数。 像这样:

 val binder = Binder(lifecycle) 

现在,您无需担心订阅-它们将在生命周期结束时被删除。 同时,生命周期可以重复很多次(例如Android UI中的开始和停止周期),而Binder每次都会为您创建和删除订阅。

什么是生命周期?


大多数Android开发人员在看到“生命周期”这个短语时,就分别代表了活动和片段周期。 是的, Binder可以与他们合作,在周期结束时退订。

但这仅仅是开始,因为您不以任何方式使用Android接口LifecycleOwner - Binder具有自己的,更通用的接口。 本质上是BEGIN / END信号流:

 interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END } // Remainder omitted } 

您可以使用Observable(通过映射)实现此流,也可以仅将库中的ManualLifecycle类用于非Rx环境(请参见下文)。

活页夹如何工作 ? 接收到BEGIN信号后,它将为您先前配置的组件( 输入/输出 )创建订阅,并接收END信号,将其删除。 最有趣的是,您可以重新开始:

 val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7") // will print: // 2 // 3 // 5 // 6 

在使用Android时,除了通常的Create-Destroy之外,还可以有多个Start-Stop和Resume-Pause周期时,这种重新分配订阅的灵活性特别有用。

Android活页夹生命周期


库中有三个类:

  • CreateDestroyBinderLifecycleandroidLifecycle
  • StartStopBinderLifecycleandroidLifecycle
  • ResumePauseBinderLifecycle e( androidLifecycle

androidLifecyclegetLifecycle()方法返回的值,即AppCompatActivityAppCompatDialogFragment等。一切都很简单:

 fun createBinderForActivity(activity: AppCompatActivity) = Binder(   CreateDestroyBinderLifecycle(activity.lifecycle) ) 

个人生命周期


我们不要止步于此,因为我们丝毫不依赖于Android。 粘合剂的生命周期是多少? 从字面上看,例如:对话框的回放时间或某些异步任务的执行时间。 您可以说,将其绑定到DI作用域-然后所有订阅都将随之删除。 完全的行动自由。

  1. 是否希望在Observable发送项目之前保存订阅? 将此对象转换为Lifecycle并将其传递给Binder 。 在扩展功能中实现以下代码,并在以后使用:

     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) ) 
  2. 想要保留您的绑定,直到“完成”完成? 没问题-这可以通过与上一段类似的方式来完成:

     fun Completable.toBinderLifecycle() = Lifecycle.wrap(   Observable.concat(       Observable.just(BEGIN),       this.andThen(Observable.just(END))   ) ) 
  3. 是否需要其他一些非Rx代码来决定何时删除订阅? 如上所述使用ManualLifecycle

在任何情况下,您都可以将响应流放置到Lifecycle.Event元素流中,或者如果使用的是非Rx代码,则可以使用ManualLifecycle

总体系统概述


活页夹隐藏了创建和管理Rx订阅的详细信息。 剩下的只是一个简短的概括概述:“组件A与范围C中的组件B交互”。

假设当前屏幕具有以下反应性组件:



我们希望将组件连接到当前屏幕中,并且我们知道:

  • UIEvent可以直接提供给AnalyticsTracker
  • UIEvent可以转换为Wish for Feature ;
  • 可以将状态转换为ViewViewModel

这可以用几行来表示:

 with(binder) {   bind(feature to view using stateToViewModelTransformer)   bind(view to feature using uiEventToWishTransformer)   bind(view to analyticsTracker) } 

我们进行这样的挤压来演示组件的互连。 而且由于我们的开发人员花在编写代码上的时间多于编写代码,所以这种简短的概述非常有用,尤其是随着组件数量的增加。

结论


我们看到了Binder如何帮助管理Rx订阅,以及它如何帮助您概述由响应组件构建的系统。

在以下文章中,我们将介绍如何将反应式UI组件与业务逻辑分开,以及如何使用Binder添加中间对象(用于日志记录和时间旅行调试)。 不要切换!

同时,请查看GitHub上的库。

Source: https://habr.com/ru/post/zh-CN430550/


All Articles