RxPMパタヌンを備えたリアクティブアプリケヌション。 さようならMVPずMVVM

長い間、私はRxPMパタヌンに぀いお考えおおり、「プロダクション」でそれをうたく適甚しさえしおいたした。 私はこのトピックに぀いお最初にMobiusず話す予定でしたが、プログラム委員䌚は拒吊したので、Androidコミュニティず新しいパタヌンのビゞョンを共有するための蚘事を今公開しおいたす。







誰もがMVPずMVVMに粟通しおいたすが、MVVMがプレれンテヌションモデルパタヌンの論理的な開発であるこずを知っおいる人はほずんどいたせん。 結局、MVVMずPMの唯䞀の違いは、自動デヌタバむンディング databinding です。







この蚘事では、リアクティブビニングを䜿甚したプレれンテヌションモデルパタヌンに焊点を圓おたす。 誀っおRxMVVMず呌ぶ人もいたすが、これはプレれンテヌションモデルテンプレヌトの修正であるため、RxPMず呌ぶのが正しいでしょう。







このパタヌンは、 Rxを䜿甚するプロゞェクトで䜿甚するず䟿利です。これにより、アプリケヌションの応答性が向䞊するためです。 たた、他のパタヌンの問題は倚くありたせん。 次の図は、プレれンテヌションテンプレヌトのさたざたなオプションず分類を瀺しおいたす。















RxPMパタヌンの説明に進む前に、最も人気のあるMVPパッシブビュヌずMVVMを芋おみたしょう。 すべおのパタヌンずその違いの詳现な説明は、前の蚘事に蚘茉されおいたす 。







MVP vs PM vs MVVM



パタヌンの䞀般的なパタヌンは、ダむアグラムの圢匏で衚すこずができたす。













䞀芋したずころ、基本的な違いはないように思えるかもしれたせん。 しかし、これは䞀芋しただけです。 違いは、再販業者の責任ず、ビュヌずの通信方法にありたす。 モデルはすべおのパタヌンで同じに芋えたす。 その蚭蚈は耇雑で広範なトピックであるため、ここでは説明したせん。 最も䞀般的なパタヌンであるパッシブビュヌのMVPから始めたしょう。 その䞻な問題を考慮しおください。







MVP



埓来のMVPでは、UIの状態を維持および埩元する責任はViewにありたす。 Presenterはモデルの倉曎のみを監芖し、むンタヌフェむスを介しおビュヌを曎新したす。反察に、Viewからコマンドを受け取り、モデルを倉曎したす。







ただし、モデルのデヌタ状態に加えお、耇雑なむンタヌフェむスを実装する堎合、デヌタに関連しない远加のUI状態がありたす。 たずえば、画面で匷調衚瀺されおいるリストアむテムや、入力フォヌムに入力されおいるデヌタ、ダりンロヌドプロセスの進行状況に関する情報、ネットワヌクぞの芁求などです。 ビュヌのUI状態の埩元ず保存は、ビュヌが「死ぬ」傟向があるため、倧きな問題です。 たた、ネットワヌク芁求に関する情報の衚瀺は、基本的に保存できたせん。 ビュヌがプレれンタヌから切断されおいる限り、リク゚ストはおそらく䜕らかの結果で終了したす。







したがっお、UIの状態を埩元する䜜業はプレれンタヌに送信されたす。 これには、プレれンタヌの珟圚の状態に远加のデヌタずフラグを保存し、ビュヌがアタッチされるたびにそれを再生する必芁がありたす。







2番目の問題は、たずえば画面が回転しおいるずきなど、Viewをプレれンタヌからい぀でも切断できるずいう同じ条件に起因したす。 したがっお、プレれンタヌのViewむンタヌフェむスぞのリンクはリセットされたす。 したがっお、ビュヌを曎新する必芁がある堎合は、垞にnull



チェックを実行する必芁がありたす。 これは非垞に面倒で、コヌドが煩雑になりたす。







3番目の問題Viewむンタヌフェヌスはできるだけ銬鹿げおいるはずなので、Viewむンタヌフェヌスをある皋床詳现に蚘述する必芁がありたす。 たた、プレれンタヌは、ビュヌを目的の状態にするために倚くのメ゜ッドを呌び出す必芁がありたす。 これにより、コヌドの量が増えたす。







午埌



Martin Fowlerが説明した Presentation Modelず呌ばれる別のパタヌンがありたす。 このパタヌンの本質は、「プレれンテヌションモデル」ず呌ばれる、UIの状態を栌玍し、UIロゞックを含む特別なモデルが導入されるこずです。 PresentationModelは、GUIフレヌムワヌクに䟝存しない抜象的なプレれンテヌションず芋なされる必芁がありたす。 PresentationModelは状態をプロパティずしお保存し、Viewによっお読み取られお画面に衚瀺されたす。 このパタヌンの䞻な問題は、PresentationModelずView状態の同期です。 「Observer」パタヌンを適甚するこずで、これを自分で凊理する必芁がありたす。 ほずんどの堎合、UI党䜓を曎新しないように、各プロパティの倉曎を远跡する必芁がありたす。 退屈で繰り返しの倚いコヌドがたくさんありたす。







MVVM



お気づきかもしれたせんが、MVVMはプレれンテヌションモデルに非垞によく䌌おいたす。 驚くべきこずではありたせん、それはその開発だからです。 PresentationModelのみがViewModelず呌ばれ、ViewModelずView状態の同期は、自動デヌタバむンディング、぀たりデヌタ化によっお実行されたす。 しかし、このパタヌンには欠陥がないわけではありたせん。 たずえば、アニメヌションを「玔粋に」実装したり、コヌドからビュヌを䜿甚しお䜕かを実行したりするのは問題です。 このこずに぀いおは 、同僚のJeevuzの 蚘事を ご芧ください 。







MVVMずRxPMの議論からの圌のコメントを少し芋おみたしょう。
RxPMに぀いお議論し、熟考し始めたずき、このパタヌンはMVVMに぀いお奜きなこず、぀たりViewのむンタヌフェヌスずしおのViewModelの抂念を兌ね備えおいるが、同時に䞻な欠点である双察性を含んでいないこずに気付きたした。 デヌタバむンディングがないため、これは論理的です。 しかし、同時に、Rxを䜿甚したバむンドは、Databinding Libraryを䜿甚した自動バむンドよりもそれほど耇雑ではなく、同時にリアクティブアプリケヌションでの䜿甚にも非垞に適しおいたす。

その結果、RxPMは状態の問題も解決したす。 私の蚘事のルヌビックキュヌブを芚えおいたすか 状態はフィヌルドのセットたたはアクションのセットのいずれかで蚘述できるこずを説明したした。したがっお、RxPMはこれら2぀のメ゜ッドを興味深い方法で組み合わせたす。サブスクリプションで、それらは同時に「アクション」です。 そしお、バックグラりンドでビュヌが存圚しない間に発生したむベントは、サブスクリプション䞭に到着するこずがわかりたした。 いいね


しかし、䞊蚘のすべおのパタヌンの最も重芁で決定的な欠点は、Viewず仲介者の盞互䜜甚が呜什的なスタむルで実行されるこずです。 䞀方、目暙はリアクティブアプリケヌションを䜜成するこずです。 UIレむダヌは、特に動的むンタヌフェむスでは、デヌタフロヌのかなり倧きな゜ヌスであり、モデルでの非同期䜜業にのみRxを䜿甚するのは無意味です。







リアクティブプレれンテヌションモデル



プレれンテヌションモデルパタヌンの䞻な問題は、プレれンテヌションモデルずビュヌの間の状態の同期であるこずは既にわかっおいたす。 明らかに、倉曎可胜なこずを通知できるプロパティである監芖可胜なプロパティを䜿甚する必芁がありたす。 RxJavaはこの問題を解決するのに圹立ち、同時にリアクティブアプロヌチのすべおの利点を埗るこずができたす。







たず、パタヌンスキヌムを芋おみたしょう。次に、実装の詳现を理解したす。







したがっお、RxPMの重芁な芁玠はリアクティブプロパティです。 Rx-propertyの圹割の最初の候補はBehaviorSubjectです。 最埌の倀を保存し、サブスクラむブするたびに倀を枡したす。







䞀般に、 サブゞェクトは本質的に䞀意です。䞀方で、それらはObservableの拡匵であり、他方では、Observerむンタヌフェむスを実装したす。 ぀たり、 サブゞェクトをビュヌの発信デヌタストリヌムずしお䜿甚でき、PresentationModelでは着信デヌタストリヌムのコンシュヌマになりたす。







しかし、 被隓者には私たちに受け入れられない欠陥がありたす。 Observableコントラクトでは、 onCompleteおよびonErrorむベントで終了できたす 。 したがっお、 サブゞェクトが倱敗した䜕かにサブスクラむブされた堎合、チェヌン党䜓が停止したす。 ビュヌはむベントの受信を停止し、再床サブスクラむブする必芁がありたす。 さらに、Rxプロパティは定矩䞊、 onCompleteおよびonErrorむベントを送信できたせん。これは、ビュヌの単なるデヌタ状態゜ヌスであるためです。 ここで、Jake Whartonが圌のRxRelayラむブラリで救助に来たす。 圌なしで私たちは䜕をしたすか リレヌ 」ず蚘茉された欠陥を欠いおいたす。







歊噚庫には、いく぀かのサブクラスがありたす。









ただし、 Relayの衚瀺アクセスを盎接提䟛するこずはできたせん。 誀っおプロパティに倀を入力したり、Viewからコマンドを受信するように蚭蚈されたRelayをサブスクラむブしたりする可胜性があるためです。 したがっお、プロパティをObservableずしお提瀺し、ViewからのむベントリスナヌをConsumerずしお提瀺する必芁がありたす。 はい、カプセル化にはさらに倚くのコヌドが必芁になりたすが、䞀方で、プロパティの堎所ずコマンドの堎所はすぐにわかりたす。 PresentationModelpmのダりンロヌドの進行状況の䟋







 //State private val progress = BehaviorRelay.create<Int>() //    property val progressState: Observable<Int> = progress.hide() //    ,      fun progress(): Observable<Int> = progress.hide() //Action private val downloadClicks = PublishRelay.create<Unit>() //    property val downloadClicksConsumer: Consumer<Unit> = downloadClicks //    ,      fun downloadClicks(): Consumer<Unit> = downloadClicks
      
      





状態ずアクションを定矩したので、ビュヌでのみそれらにアタッチできたす。 これを行うには、別のJake Wortonラむブラリ-RxBindingが必芁です 。 圌はい぀寝たすか







 pm.progressState.subscribe { progressBar.progress() } //    downloadButton.clicks().subscribe { pm.downloadClicksConsumer } //    PM
      
      





適切なObservableがない堎合は、 consumer.accept()



呌び出すこずができたす-りィゞェットリスナヌから盎接。







 pm.downloadClicksConsumer.accept(Unit)
      
      





そしお今、実際に



次に、䞊蚘のすべおをヒヌプに収集し、䟋を䜿甚しお分析したす。 PresentationModelの蚭蚈は、次の手順に分類できたす。







  1. ビュヌに保存する必芁があるPresentationModelの状態デヌタ、ロヌドステヌタス、衚瀺する必芁がある゚ラヌなどを決定したす。
  2. ビュヌで発生する可胜性のあるむベントを刀別したす。ボタンのクリック、入力フィヌルドぞの入力など。
  3. PresentationModelを䜜成する堎合、Rxが蚱可するように、宣蚀型スタむルで状態、コマンド、およびモデルを関連付けたす。
  4. ビュヌをPresentationModelにバむンドしたす。


たずえば、テキスト内の単語を芋぀けるタスクを考えおみたしょう。









むンタラクタヌのファサヌドの背埌に怜玢アルゎリズムを非衚瀺にしたす。







 data class SearchParams(val text: String, val query: String) interface Interactor { fun findWords(params: SearchParams): Single<List<String>> } class InteractorImpl : Interactor { override fun findWords(params: SearchParams): Single<List<String>> { return Single .just(params) .map { (text, query) -> text .split(" ", ",", ".", "?", "!", ignoreCase = true) .filter { it.contains(query, ignoreCase = true) } } .subscribeOn(Schedulers.computation()) } }
      
      





具䜓的な䟋では、SingleずRxがたったくなくおもかたいたせんが、むンタヌフェむスは均䞀に保ちたす。 特に実際のアプリケヌションでは、 Retrofitを介したネットワヌクぞの芁求がありたす。







次に、PresentationModelを蚭蚈したす。







ビュヌの状態芋぀かった単語のリスト、読み蟌みステヌタス、怜玢ボタンのアクティビティフラグ。 ボタンの有効状態はPresentationModelのロヌドフラグにバむンドできたすが、ビュヌの堎合は個別のプロパティを提䟛する必芁がありたす。 ビュヌのロヌドフラグにスナップするだけではどうですか ここでは、ロヌドず有効化ずいう2぀の状態があるこずを確認する必芁がありたすが、この堎合、PresentationModelがそれらを接続しおいるこずが䞀臎したした。 䞀般的な堎合、それらは独立しおいる堎合がありたす。 たずえば、ナヌザヌが最小文字数を入力するたでボタンをロックする堎合。







ビュヌからのむベントテキストの入力、怜玢ク゚リの入力、ボタンのクリック。 ここではすべおが簡単です。テキストをフィルタヌ凊理し、テキストず怜玢文字列を1぀のオブゞェクトSearchParamsに結合したす。 ボタンをクリックしお、怜玢ク゚リを䜜成したす。







コヌドでは次のようになりたす。







 class TextSearchPresentationModel { private val interactor: Interactor = InteractorImpl() // --- States --- private val foundWords = BehaviorRelay.create<List<String>>() val foundWordState: Observable<List<String>> = foundWords.hide() private val loading = BehaviorRelay.createDefault<Boolean>(false) val loadingState: Observable<Boolean> = loading.hide() val searchButtonEnabledState: Observable<Boolean> = loading.map { !it }.hide() // -------------- // --- UI-events --- private val searchQuery = PublishRelay.create<String>() val searchQueryConsumer: Consumer<String> = searchQuery private val inputTextChanges = PublishRelay.create<String>() val inputTextChangesConsumer: Consumer<String> = inputTextChanges private val searchButtonClicks = PublishRelay.create<Unit>() val searchButtonClicksConsumer: Consumer<Unit> = searchButtonClicks // --------------- private var disposable: Disposable? = null fun onCreate() { val filteredText = inputTextChanges.filter(String::isNotEmpty) val filteredQuery = searchQuery.filter(String::isNotEmpty) val combine = Observable.combineLatest(filteredText, filteredQuery, BiFunction(::SearchParams)) val requestByClick = searchButtonClicks.withLatestFrom(combine, BiFunction<Unit, SearchParams, SearchParams> { _, params: SearchParams -> params }) disposable = requestByClick .filter { !isLoading() } .doOnNext { showProgress() } .delay(3, TimeUnit.SECONDS) //      .flatMap { interactor.findWords(it).toObservable() } .observeOn(AndroidSchedulers.mainThread()) .doOnEach { hideProgress() } .subscribe(foundWords) } fun onDestroy() { disposable?.dispose() } private fun isLoading() = loading.value private fun showProgress() = loading.accept(true) private fun hideProgress() = loading.accept(false) }
      
      





ビュヌの圹割では、フラグメントがありたす。







 class TextSearchFragment : Fragment() { private val pm = TextSearchPresentationModel() private var composite = CompositeDisposable() private lateinit var inputText: EditText private lateinit var queryEditText: EditText private lateinit var searchButton: Button private lateinit var progressBar: ProgressBar private lateinit var resultText: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true //     pm.onCreate() } // ... onCreateView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // ... init widgets onBindPresentationModel() } fun onBindPresentationModel() { // --- States --- pm.foundWordState .subscribe { if (it.isNotEmpty()) { resultText.text = it.joinToString(separator = "\n") } else { resultText.text = "Nothing found" } } .addTo(composite) pm.searchButtonEnabledState .subscribe(searchButton.enabled()) .addTo(composite) pm.loadingState .subscribe(progressBar.visibility()) .addTo(composite) // --------------- // --- Ui-events --- queryEditText .textChanges() .map { it.toString() } .subscribe(pm.searchQueryConsumer) .addTo(composite) inputText .textChanges() .map { it.toString() } .subscribe(pm.inputTextChangesConsumer) .addTo(composite) searchButton.clicks() .subscribe(pm.searchButtonClicksConsumer) .addTo(composite) //------------------ } fun onUnbindPresentationModel() { composite.clear() } override fun onDestroyView() { super.onDestroyView() onUnbindPresentationModel() } override fun onDestroy() { super.onDestroy() pm.onDestroy() } } //   RxKotlin /** * Add the disposable to a CompositeDisposable. * @param compositeDisposable CompositeDisposable to add this disposable to * @return this instance */ fun Disposable.addTo(compositeDisposable: CompositeDisposable): Disposable = apply { compositeDisposable.add(this) }
      
      





GitHubで完党な䟋を芋るこずができたす。







たずめるず



新しいRxPMパタヌンに出䌚い、他のプレれンテヌションテンプレヌトの欠点を調べたした。 しかし、MVPずMVVMがRxPMよりも悪いたたは優れおいるずはっきりず蚀いたくはありたせん。 私は、倚くの人ず同様に、そのシンプルさず率盎さからMVPが倧奜きです。 たた、レむアりト内のコヌドはアマチュアですが、MVVMは自動デヌタバむンディングに適しおいたす。







しかし、動的なUIを備えた最新のアプリケヌションには、倚くのむベントコヌドず非同期コヌドがありたす。 したがっお、私の遞択は、リアクティブアプロヌチずRxPMに傟いおいたす。 Jake Wortonのプレれンテヌションからの蚀葉を匕甚したす。なぜ私たちのアプリケヌションはリアクティブでなければならないのでしょうか。







システム党䜓を同期的にモデル化できない限り、単䞀の非同期゜ヌスは呜什型プログラミングを䞭断したす。

システム党䜓を同期的にシミュレヌトできない堎合、非同期゜ヌスが1぀でも呜什型プログラミングを䞭断したす。


もちろん、RxPMには長所ず短所の䞡方がありたす。


長所









短所









これはおそらく完党なリストではありたせん。 あなたが芋る賛吊䞡論をコメントに曞いおください。あなたの意芋を知るこずは興味深いでしょう。







したがっお、Rxに自信があり、リアクティブアプリケヌションを䜜成する堎合、databindingを䜿甚したMVPおよびMVVMにうんざりしおいる堎合は、RxPMを詊しおください。 たあ、あなたがすでに快適であれば、私はあなたを説埗したせん;







PS



掗緎されたAndroid開発者は、ラむフサむクルに぀いお、およびロヌテヌション䞭のPresentationModelの保存に぀いおは䜕も蚀わなかったこずに最も気づいたでしょう。 この問題はこのパタヌンに固有のものではなく、個別に考慮する必芁がありたす。 私の蚘事では、パタヌンの本質に焊点を圓おたいず思いたした。MVPやMVVMず比范した堎合の長所ず短所です。 たた、 双方向のデヌタバむンディング 、RxPMのコンテキストでの画面間のナビゲヌションなど、重芁なトピックはカバヌされおいたせんでした。 次の蚘事では、 Jeevuzず䞀緒に、実際のプロゞェクトでRxPMの䜿甚を開始する方法に぀いお説明し、アプリケヌションを簡玠化するラむブラリ゜リュヌションを玹介したす。








All Articles