こんにちは 多くのAndroidアプリケーションは、サーバーからデータをダウンロードし、現時点ではダウンロードインジケーターを表示し、その後、データを更新できます。 アプリケーションには数十の画面があり、そのほとんどすべてに必要なものがあります。
- 画面に行くとき、データがサーバーからロードされている間、ロードインジケーター(
ProgressBar
)を表示します; - ダウンロードエラーの場合、エラーメッセージと「ダウンロードを再試行」ボタンを表示します
- ダウンロードが成功した場合は、ユーザーにデータを更新する機会を与えます(
SwipeRefreshLayout
)。 - データの更新中にエラーが発生した場合、対応するメッセージ(
Snackbar
)を表示します。
アプリケーションを開発するときは、 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
パラメータは次のとおりです。
K
モデルの初期部分がロードされるキーです。- モデルの最初の部分を入力します。
M
タイプのモデル。- この
Presenter
するV
タイプのView
。
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にあります。
ご清聴ありがとうございました!