モデルビューインテントとダウンロード/更新インジケーター

こんにちは 多くのAndroidアプリケーションは、サーバーからデータをダウンロードし、現時点ではダウンロードインジケーターを表示し、その後、データを更新できます。 アプリケーションには数十の画面があり、そのほとんどすべてに必要なものがあります。



アプリケーションを開発するときは、 Mosby実装でMVIアーキテクチャ(Model-View-Intent)を使用します。これについては、 Habréで詳細を読むか、mosby開発者のサイトでMVIに関する元の記事を見つけてください。 この記事では、上記のロード/更新ロジックを他のデータアクションから分離できるようにする基本クラスの作成について説明します。


基本クラスの作成から始める最初のことは、MVIで重要な役割を果たすViewStateの作成です。 ViewStateには、ビューの現在の状態に関するデータが含まれています(アクティビティ、フラグメント、またはViewGroup可能性がありViewGroup )。 ダウンロードと更新に関して、画面の状態がどうなるかを考えると、 ViewStateは次のようになります。


 //    LR    Load-Refresh. data class LRViewState<out M : InitialModelHolder<*>>( val loading: Boolean, val loadingError: Throwable?, val canRefresh: Boolean, val refreshing: Boolean, val refreshingError: Throwable?, val model: M ) 

最初の2つのフィールドには、ダウンロードの現在のステータスに関する情報が含まれています(ダウンロードが進行中かどうか、およびエラーが発生したかどうか)。 次の3つのフィールドには、データの更新に関する情報が含まれています(ユーザーがデータを更新できるかどうか、更新が現在行われているかどうか、エラーが発生したかどうか)。 最後のフィールドは、ロード後に画面に表示されるモデルです。


LRViewStateモデルはInitialModelHolderインターフェイスを実装します。これについては、これから説明します。
画面に表示されるデータや、画面内で何らかの形で使用されるデータをすべてサーバーからダウンロードする必要はありません。 たとえば、サーバーからダウンロードされるユーザーのリストと、リスト内のユーザーを並べ替える順序またはフィルターするいくつかの変数で構成されるモデルがあります。 ユーザーは、リストがサーバーからダウンロードされる前であっても、ソートおよび検索パラメーターを変更できます。 この場合、リストはモデルの初期(初期)部分であり、ロードに時間がかかり、ロード時にProgressBarを表示する必要があります。 InitialModelHolderインターフェイスを使用して、モデルのどの部分がソースであるかを強調するためです。


 interface InitialModelHolder<in I> { fun changeInitialModel(i: I): InitialModelHolder<I> } 

ここで、パラメーターIはモデルの初期部分I示し、モデルクラスを実装するchangeInitialModel(i: I)メソッドを使用すると、その初期(初期)部分がメソッドに渡されるもので置き換えられる新しいモデルオブジェクトを作成できますパラメータi


モデルの一部を別の部分に変更する必要がある理由は、MVI- State Reducerの主な利点の1つ(詳細はこちら )を思い出すと明らかになります。 State Reducerを使用すると、既存のViewState 部分的な変更を適用して、 ViewStateの新しいインスタンスを作成できます。 将来的には、 changeInitialModel(i: I)メソッドがState Reducerで使用され、ロードされたデータで新しいViewStateインスタンスが作成されます。


ここで、部分的な変更について話します。 部分的な変更には、 ViewStateで何を変更する必要があるかに関する情報が含まれViewState 。 すべての部分的な変更は、 PartialChangeインターフェイスを実装します。 このインターフェイスはMosbyの一部ではなく、すべての部分的な変更(ダウンロード/更新に関連するものと適用されないもの)が共通の「ルート」を持つように設計されています。


部分的な変更は、 sealedクラスに簡単に結合されます。 さらに、 LRViewStateに適用できる部分的な変更を確認できます。


 sealed class LRPartialChange : PartialChange { object LoadingStarted : LRPartialChange() //   data class LoadingError(val t: Throwable) : LRPartialChange() //     object RefreshStarted : LRPartialChange() //   data class RefreshError(val t: Throwable) : LRPartialChange() //     //      data class InitialModelLoaded<out I>(val i: I) : LRPartialChange() } 

次のステップは、ビューの基本的なインターフェースを作成することです。


 interface LRView<K, in M : InitialModelHolder<*>> : MvpView { fun load(): Observable<K> fun retry(): Observable<K> fun refresh(): Observable<K> fun render(vs: LRViewState<M>) } 

ここで、 Kパラメーターは、プレゼンターがダウンロードするデータを決定するのに役立つキーです。 キーは、たとえば、エンティティIDにすることができます。 パラメーターMは、モデルのタイプ( LRViewState modelフィールドのタイプ)を定義します。 最初の3つのメソッドはインテント(MVIの観点から)であり、イベントをViewからPresenterに送信します。 renderメソッドの実装により、 ViewStateが表示さViewStateます。


LRViewStateLRViewインターフェイスができたLRViewLRViewを作成できます。 部分的に考えてみましょう。


 abstract class LRPresenter<K, I, M : InitialModelHolder<I>, V : LRView<K, M>> : MviBasePresenter<V, LRViewState<M>>() { protected abstract fun initialModelSingle(key: K): Single<I> open protected val reloadIntent: Observable<Any> = Observable.never() protected val loadIntent: Observable<K> = intent { it.load() } protected val retryIntent: Observable<K> = intent { it.retry() } protected val refreshIntent: Observable<K> = intent { it.refresh() } ... ... } 

LRPresenterパラメータは次のとおりです。



initialModelSingleメソッドの実装は、渡されたキーを使用してモデルの初期部分をロードするためにio.reactivex.Singleを返す必要がありますreloadIntentフィールドは後継クラスでオーバーライドでき、モデルの最初の部分をreloadIntentために使用されます(たとえば、特定のユーザーアクションの後)。 次の3つのフィールドは、 Viewからイベントを受信するための意図を作成しView


次に、 LRPresenterは、 io.reactivex.Observableを作成する方法LRPresenterは、ダウンロードまたは更新に関連する部分的な変更を転送します。 以下では、後継クラスがこのメソッドをどのように使用できるかを示します。


 protected fun loadRefreshPartialChanges(): Observable<LRPartialChange> = Observable.merge( Observable .merge( Observable.combineLatest( loadIntent, reloadIntent.startWith(Any()), BiFunction { k, _ -> k } ), retryIntent ) .switchMap { initialModelSingle(it) .toObservable() .map<LRPartialChange> { LRPartialChange.InitialModelLoaded(it) } .onErrorReturn { LRPartialChange.LoadingError(it) } .startWith(LRPartialChange.LoadingStarted) }, refreshIntent .switchMap { initialModelSingle(it) .toObservable() .map<LRPartialChange> { LRPartialChange.InitialModelLoaded(it) } .onErrorReturn { LRPartialChange.RefreshError(it) } .startWith(LRPartialChange.RefreshStarted) } ) 

LRPresenterの最後の部分はState Reducerです 。これは、読み込みまたは更新に関連するViewState部分的な変更を適用します(これらの部分的な変更は、 loadRefreshPartialChangesメソッドで作成されたObservableから渡されました)。


 @CallSuper open protected fun stateReducer(viewState: LRViewState<M>, change: PartialChange): LRViewState<M> { if (change !is LRPartialChange) throw Exception() return when (change) { LRPartialChange.LoadingStarted -> viewState.copy( loading = true, loadingError = null, canRefresh = false ) is LRPartialChange.LoadingError -> viewState.copy( loading = false, loadingError = change.t ) LRPartialChange.RefreshStarted -> viewState.copy( refreshing = true, refreshingError = null ) is LRPartialChange.RefreshError -> viewState.copy( refreshing = false, refreshingError = change.t ) is LRPartialChange.InitialModelLoaded<*> -> { @Suppress("UNCHECKED_CAST") viewState.copy( loading = false, loadingError = null, model = viewState.model.changeInitialModel(change.i as I) as M, canRefresh = true, refreshing = false ) } } } 

LRViewを実装するベースフラグメントまたはアクティビティを作成することは残ります。 私のアプリケーションでは、SingleActivityApplicationアプローチに従っているので、 LRFragment作成しましょう。


ロードインジケーターと更新インジケーターを表示し、ダウンロードと更新を繰り返す必要性に関するイベントを受信するために、 LoadRefreshPanelViewStateディスプレイを委任し、イベントのファサードとなるLoadRefreshPanelインターフェイスが作成されました。 したがって、後続のフラグメントにはSwipeRefreshLayoutと[ SwipeRefreshLayout再試行]ボタンは必要ありません。


 interface LoadRefreshPanel { fun retryClicks(): Observable<Any> fun refreshes(): Observable<Any> fun render(vs: LRViewState<*>) } 

デモアプリケーションでは、 LRPanelImplクラスが作成されました。これは、 SwipeRefreshLayoutが埋め込まれたSwipeRefreshLayoutです。 ViewAnimator使用ViewAnimatorと、 ProgressBar 、エラーパネル、またはモデルを表示できViewAnimator


LoadRefreshPanel LRFragmentは次のようになります。


 abstract class LRFragment<K, M : InitialModelHolder<*>, V : LRView<K, M>, P : MviBasePresenter<V, LRViewState<M>>> : MviFragment<V, P>(), LRView<K, M> { protected abstract val key: K protected abstract fun viewForSnackbar(): View protected abstract fun loadRefreshPanel(): LoadRefreshPanel override fun load(): Observable<K> = Observable.just(key) override fun retry(): Observable<K> = loadRefreshPanel().retryClicks().map { key } override fun refresh(): Observable<K> = loadRefreshPanel().refreshes().map { key } @CallSuper override fun render(vs: LRViewState<M>) { loadRefreshPanel().render(vs) if (vs.refreshingError != null) { Snackbar.make(viewForSnackbar(), R.string.refreshing_error_text, Snackbar.LENGTH_SHORT) .show() } } } 

上記のコードからわかるように、プレゼンターをアタッチするとすぐにロードが開始され、他のすべてはLoadRefreshPanel委任されLoadRefreshPanel


ここで、ダウンロード/更新ロジックを実装する必要がある画面を作成するのは簡単なタスクになります。 たとえば、人(この場合はライダー)に関する詳細を含む画面を考えてみましょう。


エンティティクラスは簡単です。


 data class Driver( val id: Long, val name: String, val team: String, val birthYear: Int ) 

詳細を含む画面のモデルクラスは、1つのエンティティで構成されます。


 data class DriverDetailsModel( val driver: Driver ) : InitialModelHolder<Driver> { override fun changeInitialModel(i: Driver) = copy(driver = i) } 

詳細を含む画面のプレゼンタークラス:


 class DriverDetailsPresenter : LRPresenter<Long, Driver, DriverDetailsModel, DriverDetailsView>() { override fun initialModelSingle(key: Long): Single<Driver> = Single .just(DriversSource.DRIVERS) .map { it.single { it.id == key } } .delay(1, TimeUnit.SECONDS) .flatMap { if (System.currentTimeMillis() % 2 == 0L) Single.just(it) else Single.error(Exception()) } override fun bindIntents() { val initialViewState = LRViewState(false, null, false, false, null, DriverDetailsModel(Driver(-1, "", "", -1)) ) val observable = loadRefreshPartialChanges() .scan(initialViewState, this::stateReducer) .observeOn(AndroidSchedulers.mainThread()) subscribeViewState(observable, DriverDetailsView::render) } } 

initialModelSingleメソッドは、渡されたidを使用してエンティティをロードするためにSingleを作成します(エラーのUIがどのように見えるかを示すために、ほぼ2回エラーがスローされます)。 bindIntentsメソッドは、 loadRefreshPartialChangesbindIntentsメソッドを使用して、部分的な変更を伝えるObservableを作成します。


詳細を含むフラグメントの作成に移りましょう。


 class DriverDetailsFragment : LRFragment<Long, DriverDetailsModel, DriverDetailsView, DriverDetailsPresenter>(), DriverDetailsView { override val key by lazy { arguments.getLong(driverIdKey) } override fun loadRefreshPanel() = object : LoadRefreshPanel { override fun retryClicks(): Observable<Any> = RxView.clicks(retry_Button) override fun refreshes(): Observable<Any> = Observable.never() override fun render(vs: LRViewState<*>) { retry_panel.visibility = if (vs.loadingError != null) View.VISIBLE else View.GONE if (vs.loading) { name_TextView.text = "...." team_TextView.text = "...." birthYear_TextView.text = "...." } } } override fun render(vs: LRViewState<DriverDetailsModel>) { super.render(vs) if (!vs.loading && vs.loadingError == null) { name_TextView.text = vs.model.driver.name team_TextView.text = vs.model.driver.team birthYear_TextView.text = vs.model.driver.birthYear.toString() } } ... ... } 

この例では、キーはフラグメント引数に格納されています。 モデルはrender(vs: LRViewState<DriverDetailsModel>)フラグメントのrender(vs: LRViewState<DriverDetailsModel>)メソッドrender(vs: LRViewState<DriverDetailsModel>)render(vs: LRViewState<DriverDetailsModel>)LoadRefreshPanelインターフェイスの実装も作成されます。これは、負荷の表示を担当します。 この例では、ブート時にProgressBarは使用されませんが、代わりにデータフィールドにドットが表示されます。これは読み込みを象徴しています。 エラーの場合はretry_panelが表示されますが、更新は提供されません( Observable.never() )。


説明したクラスを使用するデモアプリケーションは、 GitHibにあります。
ご清聴ありがとうございました!



Source: https://habr.com/ru/post/J335112/


All Articles