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

こんにちは 多くの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



ます。







LRViewState



LRView



インターフェイスができたLRView



LRView



を作成できます。 部分的に考えてみましょう。







 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



作成しましょう。







ロードインジケーターと更新インジケーターを表示し、ダウンロードと更新を繰り返す必要性に関するイベントを受信するために、 LoadRefreshPanel



ViewState



ディスプレイを委任し、イベントのファサードとなる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



メソッドは、 loadRefreshPartialChanges



bindIntents



メソッドを使用して、部分的な変更を伝える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にあります。

ご清聴ありがとうございました!








All Articles