Kotlinを使用したリアクティブコンポーネントシステムの構築





みなさんこんにちは! 私の名前はアナトリーバリボンチクです。私はBadooのAndroid開発者です。 本日、開発プロセスで日々使用しているMVIの実装に関する同僚Zsolt Kocsiによる記事の第2部の翻訳を共有します。 最初の部分はこちらです。



私たちが望むものとその方法



記事の前半では、再利用可能なMVICoreの中心的な要素であるFeaturesを紹介しました。 最もシンプルな構造で、 Reducerを 1つだけ含めることも、非同期タスク、イベントなどを管理するためのフル機能のツールにすることもできます。



各機能は追跡可能です-ステータスの変更をサブスクライブし、それに関する通知を受信する機会があります。 この場合、Featureは入力ソースにサブスクライブできます。 これは理にかなっています。コードベースにRxを含めることで、さまざまなレベルで多くの観測可能なオブジェクトとサブスクリプションが既にあるためです。



リアクティブコンポーネントの数の増加に関連して、私たちが持っているものと、システムをさらに良くすることができるかどうかを考える時が来ました。



次の3つの質問に答える必要があります。



  1. 新しい反応性コンポーネントを追加するときに使用する要素は何ですか?

  2. サブスクリプションを管理する最も簡単な方法は何ですか?

  3. ライフサイクル管理/メモリリークを回避するためにサブスクリプションをクリアする必要性を無視することは可能ですか? つまり、コンポーネントバインディングをサブスクリプション管理から分離できますか?



記事のこのパートでは、リアクティブコンポーネントを使用してシステムを構築する基本と利点を調べ、Kotlinがこれをどのように支援するかを見ていきます。



主な要素



Featuresの設計と標準化に取り掛かる頃には、すでに多くの異なるアプローチを試し、 Featureがリアクティブコンポーネントの形になると判断していました。 まず、主なインターフェイスに注目しました。 まず、入力データと出力データのタイプを決定する必要がありました。



次のように推論しました:





その結果、出力にObservableSource <T>を使用し、入力にConsumer <T>を使用することにしました。 どうしてObservable / Observerじゃないの? 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は、4つのメソッド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; }
      
      





したがって、2つのインターフェイスがあり、それぞれに1つのメソッドが含まれています。 これで、 Consumer <T>ObservableSource <T>に署名することでそれらをバインドできます。 後者はObserver <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には、 バインダーと呼ばれるクラスが含まれています。このクラスは、Rxサブスクリプションを管理するためのシンプルなAPIを提供し、 多くの優れた機能を備えています。



なぜ必要なのですか?





手動で署名する代わりに、上記の例を次のように書き換えることができます。



 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が好きなもう1つの理由です)。 しかし、 バインダーは構文糖としてだけでなく、ライフサイクルの問題を解決するのにも役立ちます。



バインダーを作成



インスタンスの作成はどこにも簡単ではありません。



 val binder = Binder()
      
      





ただし、この場合、手動でサブスクbinder.dispose()



を解除する必要があり、サブスクリプションを削除する必要がある場合はいつでもbinder.dispose()



を呼び出す必要があります。 別の方法があります:ライフサイクルインスタンスをコンストラクタに注入します。 このように:



 val binder = Binder(lifecycle)
      
      





これで、サブスクリプションについて心配する必要はありません-サブスクリプションはライフサイクルの終了時に削除されます。 同時に、ライフサイクルを何度も繰り返すことができます(Android UIの開始および停止サイクルなど)- バインダーは毎回サブスクリプションを作成および削除します。



そして、ライフサイクルとは何ですか?



ほとんどのAndroid開発者は、「ライフサイクル」というフレーズを見て、アクティビティとフラグメントのサイクルを表しています。 はい、 バインダーはそれらと連携し、サイクルの終了時に登録を解除できます。



しかし、これはほんの始まりに過ぎません。なぜなら、AndroidインターフェースのLifecycleOwnerは決して使用しないからです。Binderには独自のより普遍的なインターフェースがあります。 基本的にはBEGIN / END信号ストリームです。



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





Observableを使用して(マッピングにより)このストリームを実装するか、非Rx環境用のライブラリのManualLifecycleクラスを使用することができます(以下を参照)。



バインダーはどのように機能しますか? BEGINシグナルを受信すると、以前に構成したコンポーネント( input / output )のサブスクリプションを作成し、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
      
      





サブスクリプションの再割り当てにおけるこの柔軟性は、通常のCreate-Destroyに加えて、複数のStart-StopおよびResume-Pauseサイクルが存在する可能性があるAndroidで作業する場合に特に役立ちます。



Androidバインダーのライフサイクル



ライブラリには3つのクラスがあります。





androidLifecycle



は、 getLifecycle()



メソッドによって返される値、つまりAppCompatActivityAppCompatDialogFragmentなどです。すべてが非常に単純です。



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





個々のライフサイクル



私たちはAndroidには一切関わっていないので、そこで止まらないでください。 バインダーのライフサイクルとは何ですか? 文字通り何でも:例えば、ダイアログの再生時間または非同期タスクの実行時間。 たとえば、DIスコープにバインドできます。そうすると、サブスクリプションはすべて削除されます。 行動の完全な自由。



  1. Observableがアイテムを送信する前にサブスクリプションを保存したいですか? このオブジェクトをライフサイクルに変換し、 バインダーに渡します。 拡張機能に次のコードを実装し、後で使用します。



     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) )
          
          



  2. Completableが終了するまでバインディングを保持したいですか? 問題ありません-これは前の段落との類推によって行われます:



     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と対話する」という簡潔で一般的な概要です。



現在の画面に次のリアクティブコンポーネントがあるとします。







現在の画面内でコンポーネントを接続したいので、次のことがわかっています。





これは、数行で表現できます。



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





コンポーネントの相互接続を示すために、このようなスクイーズを作成します。 また、開発者はコードを書くよりもコードの読み取りに多くの時間を費やすため、特にコンポーネントの数が増えるにつれて、このような簡単な概要は非常に役立ちます。



おわりに



バインダがRxサブスクリプションの管理にどのように役立ち、リアクティブコンポーネントから構築されたシステムの概要を得るのにどのように役立つかを見ました。



次の記事では、リアクティブUIコンポーネントをビジネスロジックから分離する方法と、 バインダーを使用して中間オブジェクトを追加する方法(ロギングおよびタイムトラベルデバッグ用)について説明します。 切り替えないでください!



それまでの間、 GitHubのライブラリをご覧ください



All Articles