web-dev-qa-db-ja.com

Android-MVVMのViewModel状態のベストプラクティス?

私はAndroid LiveDataに沿ったMVVMパターンを使用したアプリ(おそらく変換)とViewとViewModel間のDataBindingに取り組んでいます。アプリが「成長している」ため、後者のビューはLiveDataとして保持され、ビューにサブスクライブします(もちろん、このデータはUIに必要です。EditTextsによる双方向バインディングであれ、一方向バインディングであれ)。 UIの状態を表すデータをViewModelに保持しますが、私が見つけた結果は単純で一般的なものです。このケースのベストプラクティスに関するヒントや知識を共有できる人がいるかどうかを知りたいと思います。 LiveDataとDataBindingが利用可能であることを考慮して、ViewModelにUI(View)の状態を保存する最良の方法になりますか?

11
Giordano

私は職場で同じ問題に苦労し、私たちのために働いているものを共有することができます。 Kotlinで100%開発していますので、以下のコードサンプルも同様になります。

UIの状態

ViewModelが多くのLiveDataプロパティで肥大化するのを防ぐには、ビュー(ViewStateまたはActivity)に単一のFragmentを公開します観察する。複数のLiveDataによって以前に公開されたデータと、ビューが正しく表示するために必要なその他の情報が含まれている場合があります。

data class LoginViewState (
    val user: String = "",
    val password: String = "",
    val checking: Boolean = false
)

状態に不変のプロパティを持つDataクラスを使用しており、意図的にAndroidリソースを使用しないでください。これはMVVMに固有のものではありませんが、不変のビューステートはUIを妨げます。不整合とスレッド化の問題。

ViewModel内でLiveDataプロパティを作成して状態を公開し、初期化します:

class LoginViewModel : ViewModel() {
    private val _state = MutableLiveData<LoginViewState>()
    val state : LiveData<LoginViewState> get() = _state

    init {
        _state.value = LoginViewState()
    }
}

その後、新しい状態を生成するには、copy内のどこからでもKotlinのDataクラスによって提供されるViewModel関数を使用します。

_state.value = _state.value!!.copy(checking = true)

ビューで、他のLiveDataと同様に状態を確認し、それに応じてレイアウトを更新します。ビューレイヤーでは、状態のプロパティを実際のビューの可視性に変換し、Contextへのフルアクセスでリソースを使用できます。

viewModel.state.observe(this, Observer {
    it?.let {
        userTextView.text = it.user
        passwordTextView.text = it.password
        checkingImageView.setImageResource(
            if (it.checking) R.drawable.checking else R.drawable.waiting
        )
    }
})

複数のデータソースの統合

おそらく以前にViewModelでデータベースまたはネットワーク呼び出しからの結果とデータを公開したので、MediatorLiveDataを使用してこれらを単一の状態に統合できます。

private val _state = MediatorLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state

_state.addSource(databaseUserLiveData, { name ->
    _state.value = _state.value!!.copy(user = name)
})
...

データバインディング

統一された不変のViewStateはデータバインディングライブラリの通知メカニズムを本質的に破壊するため、BindingStateを拡張する変更可能なBaseObservableを使用して、変更のレイアウトを選択的に通知します。対応するrefreshを受け取るViewState関数を提供します。

更新:データバインディングライブラリが実際に変更された値のレンダリングのみをすでに処理しているため、変更された値をチェックするifステートメントを削除しました。@CarsonH​​olzheimerに感謝

class LoginBindingState : BaseObservable() {
    @get:Bindable
    var user = ""
        private set(value) {
            field = value
            notifyPropertyChanged(BR.user)
        }

    @get:Bindable
    var password = ""
        private set(value) {
            field = value
            notifyPropertyChanged(BR.password)
        }

    @get:Bindable
    var checkingResId = R.drawable.waiting
        private set(value) {
            field = value
            notifyPropertyChanged(BR.checking)
        }

    fun refresh(state: AngryCatViewState) {
        user = state.user
        password = state.password
        checking = if (it.checking) R.drawable.checking else R.drawable.waiting
    }
}

BindingStateの監視ビューでプロパティを作成し、refreshからObserverを呼び出します。

private val state = LoginBindingState()

...

viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = state

次に、状態をレイアウトの他の変数として使用します。

<layout ...>

    <data>
        <variable name="state" type=".LoginBindingState"/>
    </data>

    ...

        <TextView
            ...
            Android:text="@{state.user}"/>

        <TextView
            ...
            Android:text="@{state.password}"/>

        <ImageView
            ...
            app:imageResource="@{state.checkingResId}"/>
    ...

</layout>

高度な情報

一部のボイラープレートは、ViewStateの更新やBindingStateの変更の通知などの拡張機能と委任プロパティから確実に恩恵を受けるでしょう。

「クリーン」アーキテクチャを使用したアーキテクチャコンポーネントでの状態とステータスの処理に関する詳細情報が必要な場合は、チェックアウトできます GitHubのエッフェル

これは、不変のビューステートとViewModelおよびLiveDataを使用したデータバインディングを処理し、Androidシステム操作とビジネス使用ドキュメントは、ここで提供できるものよりも詳細に説明します。

21
Etienne Lenhart

Unidirectional Data Flowに基づいて、KotlinLiveDataを使用してパターンを設計しました。

詳細な説明については、完全なMedium投稿またはYouTubeトークをご覧ください。

Medium- LiveDataを使用したAndroid単方向データフロー

YouTube- 一方向のデータフロー-Adam Hurwitz-メデジンAndroid Meetup

コードの概要

ステップ1/6-Modelsモデルの定義

ViewState.kt

// Immutable ViewState attributes.
data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)

// View sends to business logic.
sealed class ViewEvent {
  data class ScreenLoad(...) : ViewEvent()
  ...
}

// Business logic sends to UI.
sealed class ViewEffect {
  class UpdateAds : ViewEffect() 
  ...
}

ステップ2/6-eventsViewModelにイベントを渡す

Fragment.kt

private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEvent
private val _viewEvent = MutableLiveData<Event<ViewEvent>>()

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    if (savedInstanceState == null)
      _viewEvent.value = Event(ScreenLoad(...))
}

override fun onResume() {
  super.onResume()
  viewEvent.observe(viewLifecycleOwner, EventObserver { event ->
    contentViewModel.processEvent(event)
  })
}

ステップ3/6-イベントの処理

ViewModel.kt

val viewState: LiveData<ViewState> get() = _viewState
val viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffect

private val _viewState = MutableLiveData<ViewState>()
private val _viewEffect = MutableLiveData<Event<ViewEffect>>()

fun processEvent(event: ViewEvent) {
    when (event) {
        is ViewEvent.ScreenLoad -> {
          // Populate view state based on network request response.
          _viewState.value = ContentViewState(getMainFeed(...),...)
          _viewEffect.value = Event(UpdateAds())
        }
        ...
}

ステップ4/6-LCEパターンを使用したネットワーク要求の管理

LCE.kt

sealed class Lce<T> {
  class Loading<T> : Lce<T>()
  data class Content<T>(val packet: T) : Lce<T>()
  data class Error<T>(val packet: T) : Lce<T>()
}

Result.kt

sealed class Result {
  data class PagedListResult(
    val pagedList: LiveData<PagedList<Content>>?, 
    val errorMessage: String): ContentResult()
  ...
}

Repository.kt

fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also { lce ->
  lce.value = Lce.Loading()
  /* Firestore request here. */.addOnCompleteListener {
    // Save data.
    lce.value = Lce.Content(ContentResult.PagedListResult(...))
  }.addOnFailureListener {
    lce.value = Lce.Error(ContentResult.PagedListResult(...))
  }
}

手順5/6-LCE状態の処理

ViewModel.kt

private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...)) { 
  lce -> when (lce) {
    // SwitchMap must be observed for data to be emitted in ViewModel.
    is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/) { 
      pagedList -> MutableLiveData<PagedList<Content>>().apply {
        this.value = pagedList
      }
    }
    is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!) { 
      pagedList -> MutableLiveData<PagedList<Content>>().apply {
        this.value = pagedList
      }
    }    
    is Lce.Error -> { 
      _viewEffect.value = Event(SnackBar(...))
      Transformations.switchMap(/*Get data from Room Db.*/) { 
        pagedList -> MutableLiveData<PagedList<Content>>().apply {
          this.value = pagedList 
        }
    }
}

ステップ6/6-状態の変化を観察します!

Fragment.kt

contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
  viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
    adapter.submitList(contentList)
  })
  ...
}
1
Adam Hurwitz