私はAndroid LiveDataに沿ったMVVMパターンを使用したアプリ(おそらく変換)とViewとViewModel間のDataBindingに取り組んでいます。アプリが「成長している」ため、後者のビューはLiveDataとして保持され、ビューにサブスクライブします(もちろん、このデータはUIに必要です。EditTextsによる双方向バインディングであれ、一方向バインディングであれ)。 UIの状態を表すデータをViewModelに保持しますが、私が見つけた結果は単純で一般的なものです。このケースのベストプラクティスに関するヒントや知識を共有できる人がいるかどうかを知りたいと思います。 LiveDataとDataBindingが利用可能であることを考慮して、ViewModelにUI(View)の状態を保存する最良の方法になりますか?
私は職場で同じ問題に苦労し、私たちのために働いているものを共有することができます。 Kotlinで100%開発していますので、以下のコードサンプルも同様になります。
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ステートメントを削除しました。@CarsonHolzheimerに感謝
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システム操作とビジネス使用ドキュメントは、ここで提供できるものよりも詳細に説明します。
詳細な説明については、完全なMedium投稿またはYouTubeトークをご覧ください。
Medium- LiveDataを使用したAndroid単方向データフロー
YouTube- 一方向のデータフロー-Adam Hurwitz-メデジンAndroid Meetup
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()
...
}
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)
})
}
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())
}
...
}
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(...))
}
}
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
}
}
}
Fragment.kt
contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
adapter.submitList(contentList)
})
...
}