RxPMと他のプレゼンテーションパターンとの比較に関する最後の記事の 6か月後、 Jeevuzと私はついにRxPMライブラリ( プレゼンテーションモデルパターンの事後的な実装)を導入する準備ができました。 ライブラリの主要コンポーネントを簡単に確認して、それらの使用方法を示しましょう。
まず、一般的なスキームを見てみましょう。
- PresentationModelは、ビューの状態を保存し、UIイベントに応答して、ビューのモデルと状態を変更します。
- 状態の変更にサブスクライブを表示し、ユーザーアクションをPresentationModelに送信します。
- モデルは、ビジネスロジック、データストレージ、および検索が隠されているレイヤーです。
ライブラリの主要コンポーネントに移りましょう。
都道府県
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にバインドされている場合、 CommandはPublishRelay
と同じように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の構築元であるState 、 Action、およびCommandについて説明しました。 次に、基本クラスPresentationModel
を分析します。 ライフサイクルですべての基本的な作業を実行します。 合計で、4つのコールバックがあります。
-
onCreate
最初の作成時に呼び出され、Rxチェーンとバインド状態を初期化するのに適した場所です。 -
onBind
ビューがPresentationModelにバインドするときに呼び出されます。 -
onUnbind
ビューがPresentationModelからonUnbind
ときに呼び出されます。 -
onDestroy
-PresentationModelは作業を完了します。 リソースを解放するのに適した場所。
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つの必須メソッドのみを実装します。
-
providePresentationModel
-PresentationModelの作成時に呼び出されます。 -
onBindPresentationModel
このメソッドでは、PresentationModelプロパティにバインドする必要があります( RxBindingとbindTo
拡張機能を使用します)。
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にある機能の完全なリストにはほど遠いです。
-
PresentationModel
の基本実装。 - 画面回転中の
PresentationModel
保存。 - ライフサイクル処理、サブスクリプション、およびサブスクリプション解除。
- Conductorを含む
PmView
を実装するための基本クラス。 -
State
、Action
、Command
。 -
InputControl
、CheckContol
、ClickControl
。 -
bindTo
およびその他の便利な拡張機能をbindTo
してプロパティをバインドします。 - Googleマップで作業するための基本クラス。
ライブラリは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 2017でRxPMに関するミニトークを行います。 来て話してください。