RxPM-プレゼンテーションモデルパターンの事後的な実装



RxPMと他のプレゼンテーションパターンとの比較に関する最後の記事の 6か月後、 Jeevuzと私はついにRxPMライブラリ( プレゼンテーションモデルパターンの事後的な実装)を導入する準備ができました。 ライブラリの主要コンポーネントを簡単に確認して、それらの使用方法を示しましょう。









まず、一般的なスキームを見てみましょう。

















ライブラリの主要コンポーネントに移りましょう。







都道府県



RxPMの主なタスクは、PresentationModelのすべての状態を記述し、リアクティブスタイルでそれらと対話する機能を提供することです。 多くの場合、状態にアクセスするだけでなく、ビュー(View)を同期するためにその変更に応答する必要があります。 これを行うために、ライブラリにはリアクティブプロパティを実装するStateクラスがあります。







リアクティブプロパティは、その変更について通知し、それと対話するためのリアクティブインターフェイスを提供するプロパティの一種です。







パターンに関する記事で、状態の変更に対する表示アクセスから隠すために、2つのプロパティを記述する必要があると述べました。







private val inProgressRelay = BehaviorRelay.create() val inProgressObservable = inProgressRelay.hide()
      
      





これはパターンの迷惑な瞬間の1つだったので、 BehaviorRelay



Stateにラップし、それと対話するためにobservable



consumer



を提供することにしました。 これで1行で記述できます。







 val inProgress = State<Boolean>(initialValue = false)
      
      





ビューで、状態の変更をサブスクライブします。







 pm.inProgress.observable.bindTo(progressBar.visibility())
      
      





bindTo



リアクティブプロパティにバインドするためのライブラリ内の拡張







PresentationModel内でのみ使用可能なconsumerを介して状態を変更できます。







 inProgress.consumer.accept(true)
      
      





通常のプロパティと同様に、現在の状態値を取得できます。







 inProgress.value
      
      





リアクティブプロパティの利点は、その変化を観察できるだけでなく、他のリアクティブプロパティと関連付けて構成できることです。 したがって、他の人の変化に依存し、それに対応する新しい状態を取得します。 たとえば、ネットワークへのリクエストの間、ボタンをブロックできます。







 val inProgress = State(initialValue = false) val buttonEnabled = State(initialValue = true) inProgress.observable .map { progress -> !progress } .subscribe(buttonEnabled.consumer) .untilDestroy()
      
      





untilDestroy



は、 Disposable



CompositeDisposable



追加するPresentationModelの拡張機能
です。







別の例は、フォーム内のフィールドの完全性に応じて、ボタンを有効または無効にすることです。







 //       View: val nameChanges = Action<String>() val phoneChanges = Action<String>() val buttonEnabled = State(initialValue = false) Observable.combineLatest(nameChanges.observable, phoneChanges.observable, BiFunction { name: String, phone: String -> name.isNotEmpty() && phone.isNotEmpty() }) .subscribe(buttonEnabled.consumer) .untilDestroy()
      
      





したがって、いくつかのリアクティブプロパティ(状態)を宣言的にバインドし、他の依存プロパティを取得できます。 これは、リアクティブプログラミングの本質です。


アクション



Stateと同様にこのクラスはPublishRelay



へのアクセスをカプセル化し、ボタンのクリックや切り替えなどのユーザーアクションを記述することを目的としています。







 val buttonClicks = Action<Unit>() buttonClicks.observable .subscribe { // handle click } .untilDestroy()
      
      





論理的な質問は、PresentationModelでメソッドを記述するのは簡単ではありませんが、なぜプロパティを宣言してサブスクライブするのですか? 場合によってはこれは本当です。 たとえば、次の画面を開く、モデルを直接呼び出すなど、アクションが非常に単純な場合。 ただし、ネットワークへのリクエストをクリックし、同時に進行中にフィルターをクリックする必要がある場合は、この場合、 アクションによる相互作用望ましいです。 Actionの主な利点は、Rxチェーンを壊さないことです。 例で説明します。







メソッドのオプション:







 private var requestDisposable: Disposable? = null fun sendRequest() { requestDisposable?.dispose() requestDisposable = model.sendRequest().subscribe() } override fun onDestroy() { super.onDestroy() requestDisposable?.dispose() }
      
      





上記の例からわかるように、新しいクリックごとに前のリクエストを完了するために、各リクエストに対してDisposable



変数を宣言する必要があります。 また、 onDestroy



登録を解除することも忘れないでください。 これは、ボタンをクリックしてsendRequest



メソッドがsendRequest



たびに、新しいRxチェーンが作成さsendRequest



という事実の結果です。







アクション付きオプション:







 buttonClicks.observable .switchMapSingle { model.sendRequest() } .subscribe() .untilDestroy()
      
      





Actionを使用すると、Rxチェーンを1回初期化してサブスクライブするだけで済みます。 さらに、 debounce



filter



map



など、多数の便利なRx演算子を使用できます。







たとえば、検索する文字列を入力するときのクエリの遅延を考慮してください。







 val searchResult = State<List<Item>>() val searchQuery = Action<String>() searchQuery.observable .debounce(100, TimeUnit.MILLISECONDS) .switchMapSingle { // send request } .subscribe(searchResult.consumer) .untilDestroy()
      
      





また、 RxBindingと組み合わせて、ViewとPresentationModelをバインドするとさらに便利です。







 button.clicks().bindTo(pm.buttonClicks.consumer)
      
      





コマンド



別の重要な問題は、エラーとダイアログ、または他のコマンドの表示です。 一度実行する必要があるため、これらは状態ではありません。 たとえば、ダイアログを表示するために、 Stateは機能しません。Stateのサブスクリプションごと最後の値がそれぞれ受信されるため、新しいダイアログが毎回表示されるためです。 この問題を解決するために、 PublishRelay



をカプセル化することで目的の動作を実装するCommandクラスが作成されました。







しかし、ViewがまだPresentationModelに関連付けられていないときにコマンドを送信するとどうなりますか? このチームは負けます。 これを防ぐために、ビューが存在しないときにコマンドを蓄積し、ビューがバインドされたときにコマンドを送信するバッファーを提供しました。 ViewがPresentationModelにバインドされている場合、 CommandPublishRelay



と同じようにPublishRelay



ます。







デフォルトでは、バッファは無制限の数のコマンドを蓄積しますが、特定のバッファサイズを設定できます。







 val errorMessage = Command<String>(bufferSize = 3)
      
      





最後のコマンドのみを保存する場合:







 val errorMessage = Command<String>(bufferSize = 1)
      
      





0を指定すると、 コマンドPublishRelay



ようにPublishRelay



ます。







 val errorMessage = Command<String>(bufferSize = 0)
      
      





ビューに添付されます:







 errorMessage.observable().bindTo { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
      
      





Commandの最も実例となる作品は、大理石の図によって示されています:









デフォルトでは、ViewをPresentationModelにバインドするときにバッファーが有効になります。 しかし、open / close observable



設定することで、メカニズムを実装できます。









そのため、たとえば、Googleマップで作業している場合、Viewの準備の兆候はPresentationModelにバインドされるだけでなく、マップの準備もできます。 ライブラリには、マップを操作するための既製のコマンドが既にあります。







 val moveToLocation = mapCommand<LatLng>()
      
      





プレゼンテーションモデル



基本的なRxPMプリミティブ、つまりPresentationModelの構築元であるStateAction、およびCommandについて説明しました。 次に、基本クラスPresentationModel



を分析します。 ライフサイクルですべての基本的な作業を実行します。 合計で、4つのコールバックがあります。









lifecycleObservable



lifecycleObservable



サイクルを追跡することもできlifecycleObservable









便利なサブスクライブ解除のために、 PresentationModel



利用可能なDisposable



拡張機能があります。







 protected fun Disposable.untilUnbind() { compositeUnbind.add(this) } protected fun Disposable.untilDestroy() { compositeDestroy.add(this) }
      
      





onBind



onDestroy



それぞれ、 compositeDestroy



onDestroy



クリアします。







PresentationModel



の使用例を見てみましょう。

Pull To Refreshを介してネットワークにリクエストを送信し、画面上のデータを更新し、リクエスト中の進行状況を表示し、エラーが発生した場合はメッセージ付きのダイアログをユーザーに表示する必要があります。







最初に、Viewに必要な状態とコマンド、およびViewから受信できるカスタムイベントを決定する必要があります。







 class DataPresentationModel( private val dataModel: DataModel ) : PresentationModel() { val data = State<List<Item>>(emptyList()) val inProgress = State(false) val errorMessage = Command<String>() val refreshAction = Action<Unit>() // ... }
      
      





次に、 onCreate



メソッドでプロパティとモデルをバインドする必要があります。







 class DataPresentationModel( private val dataModel: DataModel ) : PresentationModel() { // ... override fun onCreate() { super.onCreate() refreshAction.observable //    .skipWhileInProgress(inProgress.observable) .flatMapSingle { dataModel.loadData() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) //    .bindProgress(inProgress.consumer) .doOnError { errorMessage.consumer.accept("Loading data error") } } .retry() .subscribe(data.consumer) .untilDestroy() //       refreshAction.consumer.accept(Unit) } }
      
      





エラーが受信されると、チェーンは作業を終了し、アクションは処理されなくなるため、 retry



演算子に注意してください。 retry



は、エラーが発生した場合にチェーンを再retry



ます。 ただし、 Stateからチェーンを開始する場合は使用しないように注意してください。


PMVIEW



PresentationModelが設計されると、それはビューにバインドするためだけに残ります。

ライブラリには、 PmSupportFragment



を実装するための基本クラスPmSupportActivity



PmSupportFragment



およびPmController



Conductorフレームワークのユーザー用)が既に実装されています。 それぞれがAndroidPmView



インターフェイスを実装し、必要なコールバックを対応するデリゲートにスローします。これにより、PresentationModelライフサイクルが制御され、画面の回転中に正しく保存されることが保証されます。







PmSupportFragment



、2つの必須メソッドのみを実装します。









 class DataFragment : PmSupportFragment<DataPresentationModel>() { override fun providePresentationModel() = DataPresentationModel(DataModel()) override fun onBindPresentationModel(pm: DataPresentationModel) { pm.inProgress.observable.bindTo(swipeRefreshLayout.refreshing()) pm.data.observable.bindTo { // adapter.setItems(it) } pm.errorMessage.observable.bindTo { // show alert dialog } swipeRefreshLayout.refreshes().bindTo(pm.refreshAction.consumer) } }
      
      





bindTo



bindTo



の便利な拡張機能AndroidPmView



。 これを使用すると、PresentationModelのプロパティからサブスクライブ解除してメインスレッドに切り替えることを心配する必要がありません。







Googleマップで作業するために、ライブラリには追加の基本クラスがあります: MapPmSupportActivity



MapPmSupportFragment



およびMapPmController



GoogleMap



をバインドするための別のメソッドを追加しGoogleMap









 fun onBindMapPresentationModel(pm: PM, googleMap: GoogleMap)
      
      





この方法では、地図上にピンを表示したり、場所を移動したりアニメーション化したりできます。







双方向のデータバインディング



これまで、PresentationModelが状態を変更し、Viewがそれにサブスクライブする場合、 Stateの一方向の変更のみを考慮してきました。 しかし、かなり頻繁に、両側から状態を変更する必要があります。 典型的な例は入力フィールドです。ユーザーとPresentationModelの両方がその値を変更し、初期値で初期化するか、入力をフォーマットできます。 このようなバンドルは、両面データバインディングと呼ばれます。 RxPMでの実装方法を図に示します。











ユーザーがテキストを入力する➔リスナーがトリガーされるchange変更がアクションに渡される







入力フィールドにこの双方向バンドルを実装し、ループの問題を解決するInputControl



クラスを作成しました。







PresentationModelで宣言します。







 val name = inputControl()
      
      





使い慣れたbindTo



介してViewに接続されます







 pm.name bindTo editText
      
      





フォーマッタを設定することもできます:







 val name = inputControl( formatter = { it.take(50).capitalize().replace("[^a-zA-Z- ]".toRegex(), "") } )
      
      





CheckBox



の同様のループと双方向バインディングの問題CheckBox



CheckControl



によって解決されCheckBox









Rxpm



ライブラリの主なクラスと機能を調べました。 RxPMにある機能の完全なリストにはほど遠いです。









ライブラリはKotlinで記述され、RxJava2を使用します。

RxPMはすでに実稼働環境のいくつかのアプリケーションで使用されており、作業の安定性を示しています。 しかし、私たちはそれに取り組み続け、さらなる開発と改善のための多くのアイデアがあります。 ナビゲーションに非常に便利な機能を備えたバージョン1.1が最近リリースされましたが、これについては次の記事で説明します。







RxPMの機能を理解するには、たった1つの記事では不十分です。 ソースと例を参照して、質問してみてください。 フィードバックを歓迎します。







RxPM: https : //github.com/dmdevgo/RxPM

サンプル: https : //github.com/dmdevgo/RxPM/tree/develop/sample

電報チャット: https : //t.me/Rx_PM







PS


11月24日(今週金曜日)に、 Droidcon Moscow 2017RxPMに関するミニトークを行います。 来て話してください。








All Articles