Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dialogs in Android MVVM (14.11.2019)

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Vladislav Vladislav
November 14, 2019

Dialogs in Android MVVM (14.11.2019)

Presentation from https://events.epam.com/events/mobile-people-open-android-meetup
Describes how alert dialogs can be treated in MVVM architecture and how we can use Android Databinding plugin to control their appearance.

Avatar for Vladislav

Vladislav

November 14, 2019

Other Decks in Programming

Transcript

  1. NOT another holy war about the architecture... MVC MVP MVVM

    Stop haunting me!
 I can choose architecture by myself!
  2. ...but how we integrated alert dialogs into our project with

    MVVM architecture, implemented using Android Jetpack stack.
  3. MVC Model Notifications Controller Commands Updates View state Controller manipulates

    the Model View State View Controller updates View state
  4. –John Gossman “The View is almost always defined declaratively, very

    often with a tool. By the nature of these tools and declarative languages some view state that MVC encodes in its View classes is not easy to represent.”
  5. MVVM View Model View State Notifications ViewModel Data Binding and

    Commands Send notifications Send notifications ViewModel updates the Model
  6. SingleLiveEvent • SingleLiveEvent is a LiveData which notifies only one

    observer; • Sends only new updates after subscription;
  7. SingleLiveEvent • SingleLiveEvent is a LiveData which notifies only one

    observer; • Sends only new updates after subscription; • Only calls the observer if there's an explicit call to setValue() or call();
  8. SingleLiveEvent • SingleLiveEvent is a LiveData which notifies only one

    observer; • Sends only new updates after subscription; • Only calls the observer if there's an explicit call to setValue() or call(); • When used with Void generic type you can use singleLiveEvent#call() to notify observer;
  9. SingleLiveEvent • Avoids a common problem with events: on configuration

    change (like rotation) an update can be emitted if the observer is active; • Used for events like navigation and Snackbar messages.
  10. Android Databinding <layout> <data> <variable name="viewModel" type="com.epam.StubViewModel" /> </data> <TextView

    android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text=“@string/text” /> </layout> Declare data
  11. Android Databinding <layout> <data> <variable name="viewModel" type="com.epam.StubViewModel" /> </data> <TextView

    android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text=“@{viewModel.text}” /> </layout> Bind to data
  12. Android Databinding Databinding plugin generates binding class from layout public

    class ActivityMainBindingImpl extends ActivityMainBinding { ... }
  13. Android Databinding Set content view in Activity class ActivityMain :

    AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ) } }
  14. Android Databinding Setup binding in Activity class ActivityMain : AppCompatActivity()

    { private val viewModel = StubViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ).also { it.lifecycleOwner = this it.viewModel = viewModel } } }
  15. In a nutshell • View state belongs to ViewModel; •

    View is connected with ViewModel via DataBinding.
  16. • How to implement dialog? • How to control appearance

    of dialog? • How to handle callbacks in ViewModel?
  17. Implement CommonDialogFragment class CommonDialogFragment : DialogFragment() { private val viewModel

    by viewModel<DialogViewModel>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ) = DataBindingUtil.inflate<ViewDataBinding>( inflater, R.layout.dialog_error, it, true )?.apply { lifecycleOwner = viewLifecycleOwner setVariable(BR.viewModel, viewModel) setVariable(BR.uiConfig, uiConfig) } }
  18. And navigator for it class DialogNavigator(private val fragmentManager: FragmentManager) {

    private fun FragmentManager.isFragmentNotExist(tag: String) = findFragmentByTag(tag) == null }
  19. And navigator for it class DialogNavigator(private val fragmentManager: FragmentManager) {

    fun hideDialog(tag: String) { val fragment = fragmentManager.findFragmentByTag(tag) if (fragment != null) { fragmentManager.beginTransaction().remove(fragment).commit() } } private fun FragmentManager.isFragmentNotExist(tag: String) = findFragmentByTag(tag) == null }
  20. And navigator for it class DialogNavigator(private val fragmentManager: FragmentManager) {

    fun showDialog(tag: String, uiConfig: IDialogUiConfig) { if (fragmentManager.isFragmentNotExist(tag)) { CommonDialogFragment.newInstance(uiConfig).show(fragmentManager, tag) } } fun hideDialog(tag: String) { val fragment = fragmentManager.findFragmentByTag(tag) if (fragment != null) { fragmentManager.beginTransaction().remove(fragment).commit() } } private fun FragmentManager.isFragmentNotExist(tag: String) = findFragmentByTag(tag) == null }
  21. Via SingleLiveEvent } class Solution1ViewModel( service: FirstTryFailingService, private val dialogEventBus:

    EventBus ) : BaseSolutionViewModel(service) { val dialogControlEvent = SingleLiveEvent<DialogControlEvent>()
  22. Via SingleLiveEvent } class Solution1ViewModel( service: FirstTryFailingService, private val dialogEventBus:

    EventBus ) : BaseSolutionViewModel(service) { val dialogControlEvent = SingleLiveEvent<DialogControlEvent>() override fun hideErrorDialog() { dialogControlEvent.value = DialogControlEvent.Hide(DIALOG_TAG) }
  23. Via SingleLiveEvent } class Solution1ViewModel( service: FirstTryFailingService, private val dialogEventBus:

    EventBus ) : BaseSolutionViewModel(service) { val dialogControlEvent = SingleLiveEvent<DialogControlEvent>() override fun showErrorDialog() { dialogControlEvent.value = DialogControlEvent.Show( tag = DIALOG_TAG, uiConfig = STANDARD_DIALOG_CONFIG ) } override fun hideErrorDialog() { dialogControlEvent.value = DialogControlEvent.Hide(DIALOG_TAG) }
  24. Observe it in Activity } class Solution1Activity : AppCompatActivity() {

    private val dialogNavigator: DialogNavigator by inject { parametersOf(this) } private val viewModel by viewModel<Solution1ViewModel>()
  25. Observe it in Activity } class Solution1Activity : AppCompatActivity() {

    private val dialogNavigator: DialogNavigator by inject { parametersOf(this) } private val viewModel by viewModel<Solution1ViewModel>() private fun showOrHideDialog(event: DialogControlEvent) { when (event) { is Show -> dialogNavigator.showDialog(event.tag, event.uiConfig) is Hide -> dialogNavigator.hideDialog(event.tag) } }
  26. Observe it in Activity } class Solution1Activity : AppCompatActivity() {

    private val dialogNavigator: DialogNavigator by inject { parametersOf(this) } private val viewModel by viewModel<Solution1ViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // inflate layout and setup binding observe(viewModel.dialogControlEvent, ::showOrHideDialog) } private fun showOrHideDialog(event: DialogControlEvent) { when (event) { is Show -> dialogNavigator.showDialog(event.tag, event.uiConfig) is Hide -> dialogNavigator.hideDialog(event.tag) } }
  27. Use EventBus: post in CommonDialogFragment } class CommonDialogFragment : DialogFragment()

    { private val dialogEventBus by inject<EventBus>() private val viewModel by viewModel<DialogViewModel>()
  28. Use EventBus: post in CommonDialogFragment } class CommonDialogFragment : DialogFragment()

    { private val dialogEventBus by inject<EventBus>() private val viewModel by viewModel<DialogViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) observeEmptyEvent(viewModel.positiveButtonClickEvent) { dialogEventBus.post(PositiveButtonClickEvent(tag!!)) } }
  29. And observe in ViewModel } class Solution1ViewModel( service: FirstTryFailingService, private

    val dialogEventBus: EventBus ) : BaseSolutionViewModel(service) {
  30. And observe in ViewModel } class Solution1ViewModel( service: FirstTryFailingService, private

    val dialogEventBus: EventBus ) : BaseSolutionViewModel(service) { @Subscribe(threadMode = ThreadMode.MAIN) fun onPositiveButtonClick(event: PositiveButtonClickEvent) { event.doIfTagMatches(DIALOG_TAG, ::onErrorRetry) }
  31. And observe in ViewModel } class Solution1ViewModel( service: FirstTryFailingService, private

    val dialogEventBus: EventBus ) : BaseSolutionViewModel(service) { init { dialogEventBus.register(this) } override fun onCleared() { dialogEventBus.unregister(this) } @Subscribe(threadMode = ThreadMode.MAIN) fun onPositiveButtonClick(event: PositiveButtonClickEvent) { event.doIfTagMatches(DIALOG_TAG, ::onErrorRetry) }
  32. Activity/Fragment Show dialog event Add DialogFragment Add DialogFragment ViewModel Assumes

    that dialog visible Process killed and restored FragmentManager DialogFragment stored in the manager DialogFragment Visible to the user The "Duplicated State" problem
  33. The “Abstraction" problem ViewModel must be unaware of view. But

    in the described solution it is aware of EventBus, which is an implementation detail of the view.
  34. The "Scope" problem To the solution to be highly re-usable

    and easy to integrate into any activity/ fragment we used EventBus for CommonDialogFragment and ViewModel communication. EventBus has to live in the scope width enough for both CommonDialogFragment and ViewModel i.e. in the singleton scope. Thus we need to manage lifecycle manually to prevent potential leaks.
  35. Solution Analysis • "Duplicated State" problem: both ViewModel and FragmentManager

    control appearance of the CommonDialogFragment; • "Abstraction" problem: ViewModel knows about EventBus which is an implementation detail of CommongDialogFragment. But CommonDialogFragment is just a view for ViewModel;
  36. Solution Analysis • "Duplicated State" problem: both ViewModel and FragmentManager

    control appearance of the CommonDialogFragment; • "Abstraction" problem: ViewModel knows about EventBus which is an implementation detail of CommongDialogFragment. But CommonDialogFragment is just a view for ViewModel; • "Scope" problem: to setup communication between dialog and ViewModel we use EventBus which has to live in singleton scope. We need to manually handle lifecycle;
  37. Solution Analysis • "Duplicated State" problem: both ViewModel and FragmentManager

    control appearance of the CommonDialogFragment; • "Abstraction" problem: ViewModel knows about EventBus which is an implementation detail of CommongDialogFragment. But CommonDialogFragment is just a view for ViewModel; • "Scope" problem: to setup communication between dialog and ViewModel we use EventBus which has to live in singleton scope. We need to manually handle lifecycle; • "Expandability" problem: switching to embed view for error handling requires a lot of modification. Why? It's just change of the view!
  38. Solution Analysis • "Duplicated State" problem: both ViewModel and FragmentManager

    control appearance of the CommonDialogFragment; • "Abstraction" problem: ViewModel knows about EventBus which is an implementation detail of CommongDialogFragment. But CommonDialogFragment is just a view for ViewModel; • "Scope" problem: to setup communication between dialog and ViewModel we use EventBus which has to live in singleton scope. We need to manually handle lifecycle; • "Expandability" problem: switching to embed view for error handling requires a lot of modification. Why? It's just change of the view! • "Expandability" problem: view logic is spread across both XML and fragment/activity :(
  39. The same: using CommonDialogFragment But lets create another wrapper around

    DialogNavigator class ErrorView( private val lifecycleOwner: LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver { init { lifecycleOwner.lifecycle.addObserver(this) } }
  40. Replace SingleLiveEvent with LiveData } class Solution2ViewModel( service: FirstTryFailingService )

    : BaseSolutionViewModel(service) { Notice that if has initial state val errorDialogConfig = STANDARD_DIALOG_CONFIG
  41. Replace SingleLiveEvent with LiveData } class Solution2ViewModel( service: FirstTryFailingService )

    : BaseSolutionViewModel(service) { val isDialogVisible = MutableLiveData<Boolean>(false) Notice that if has initial state val errorDialogConfig = STANDARD_DIALOG_CONFIG
  42. Replace SingleLiveEvent with LiveData } class Solution2ViewModel( service: FirstTryFailingService )

    : BaseSolutionViewModel(service) { val isDialogVisible = MutableLiveData<Boolean>(false) override fun showErrorDialog() { isDialogVisible.value = true } override fun hideErrorDialog() { isDialogVisible.value = false } val errorDialogConfig = STANDARD_DIALOG_CONFIG
  43. Observe it in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver {
  44. Observe it in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver { private fun bind(viewModel: Solution2ViewModel) { with(lifecycleOwner) { observe(viewModel.isDialogVisible, ::updateErrorDialogState) } } private fun updateErrorDialogState(visible: Boolean) { // show or hide dialog using dialogNavigator // DialogUiConfig is being created here }
  45. Observe it in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver { var viewModel: Solution2ViewModel? = null set(value) { field = value value?.let { bind(it) } } private fun bind(viewModel: Solution2ViewModel) { with(lifecycleOwner) { observe(viewModel.isDialogVisible, ::updateErrorDialogState) } } private fun updateErrorDialogState(visible: Boolean) { // show or hide dialog using dialogNavigator // DialogUiConfig is being created here }
  46. Observe events in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver {
  47. Observe events in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver { @Subscribe(threadMode = ThreadMode.MAIN) fun onPositiveButtonClick(event: PositiveButtonClickEvent) { event.doIfTagMatches(DIALOG_TAG) { viewModel?.onErrorRetry() } }
  48. Observe events in ErrorView } class ErrorView( private val lifecycleOwner:

    LifecycleOwner, private val dialogNavigator: DialogNavigator, private val dialogEventBus: EventBus ) : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { dialogEventBus.register(this) } override fun onDestroy(owner: LifecycleOwner) { dialogEventBus.unregister(this) } @Subscribe(threadMode = ThreadMode.MAIN) fun onPositiveButtonClick(event: PositiveButtonClickEvent) { event.doIfTagMatches(DIALOG_TAG) { viewModel?.onErrorRetry() } }
  49. Finally bind ErrorView to ViewModel in Activity } class Solution2Activity

    : AppCompatActivity() { private val errorView: ErrorView by inject { parametersOf(this) } private val viewModel by viewModel<Solution2ViewModel>()
  50. Finally bind ErrorView to ViewModel in Activity } class Solution2Activity

    : AppCompatActivity() { private val errorView: ErrorView by inject { parametersOf(this) } private val viewModel by viewModel<Solution2ViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // inflate layout and setup binding errorView.viewModel = viewModel }
  51. Activity/Fragment Dialog visible state Add DialogFragment Add DialogFragment Process killed

    and restored FragmentManager DialogFragment in the manager DialogFragment Visible to the user The "Duplicated State" problem - Solved ViewModel LiveData stores "false" LiveData stores "true" No Fragment Not visible Dialog invisible state Remove DialogFragment Remove DialogFragment
  52. The "Abstraction" problem - Solved To connect the helper ErrorView

    with ViewModel we use custom databinding mechanism thus ViewModel is not aware of the view’s implementation details anymore.
  53. Solution Analysis • "Duplicated State" problem: view state is stored

    only in ViewModel; • "Abstraction" problem: ViewModel knows nothing about EventBus and connects with ErrorView via databinding;
  54. Solution Analysis • "Duplicated State" problem: view state is stored

    only in ViewModel; • "Abstraction" problem: ViewModel knows nothing about EventBus and connects with ErrorView via databinding; • "Scope" problem: to setup communication between dialog and ErrorView we use EventBus which has to live in singleton scope. We need to handle lifecycle;
  55. Solution Analysis • "Duplicated State" problem: view state is stored

    only in ViewModel; • "Abstraction" problem: ViewModel knows nothing about EventBus and connects with ErrorView via databinding; • "Scope" problem: to setup communication between dialog and ErrorView we use EventBus which has to live in singleton scope. We need to handle lifecycle; • "Expandability" problem: switching to embed view requires a lot of modification. Why? It's just change of the view! :(
  56. Solution Analysis • "Duplicated State" problem: view state is stored

    only in ViewModel; • "Abstraction" problem: ViewModel knows nothing about EventBus and connects with ErrorView via databinding; • "Scope" problem: to setup communication between dialog and ErrorView we use EventBus which has to live in singleton scope. We need to handle lifecycle; • "Expandability" problem: switching to embed view requires a lot of modification. Why? It's just change of the view! :( • "Expandability" problem: view logic is spread across both XML and fragment/activity :(
  57. As a custom View } class DialogShowingView @JvmOverloads constructor( ...

    ) : View(context, attrs) { private var dialog: Dialog private var binding: ViewDataBinding init { binding = ... dialog = ... }
  58. As a custom View } class DialogShowingView @JvmOverloads constructor( ...

    ) : View(context, attrs) { private var dialog: Dialog private var binding: ViewDataBinding init { binding = ... dialog = ... } override fun setVisibility(visibility: Int) { // Show or hide dialog }
  59. As a custom View } class DialogShowingView @JvmOverloads constructor( ...

    ) : View(context, attrs) { var bindingData: Pair<DialogUiConfig?, IDialogViewModel?>? = null set(value) { // update binding } private var dialog: Dialog private var binding: ViewDataBinding init { binding = ... dialog = ... } override fun setVisibility(visibility: Int) { // Show or hide dialog }
  60. And we need some BindingAdapters @BindingAdapter(value = ["dialogConfig", "dialogViewModel"], requireAll

    = false) fun DialogShowingView.bindTextAndActions( dialogConfig: DialogUiConfig? = null, dialogViewModel: IDialogViewModel? = null ) { bindingData = Pair(dialogConfig, dialogViewModel) } @BindingAdapter("visibleOrGone") fun View.visibleOrGone(isVisible: Boolean?) { if (isVisible != null) { visibility = if (isVisible) View.VISIBLE else View.GONE } }
  61. Exactly the same VM as in previous solution class Solution3ViewModel(

    service: FirstTryFailingService ) : BaseSolutionViewModel(service) { val isDialogVisible = MutableLiveData<Boolean>(false) val errorDialogConfig = STANDARD_DIALOG_CONFIG override fun showErrorDialog() { isDialogVisible.value = true } override fun hideErrorDialog() { isDialogVisible.value = false } }
  62. Bind View to it instead of observing in code </layout>

    <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
  63. Bind View to it instead of observing in code </layout>

    <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3ViewModel" /> </data> <by.ve.dialogsbinding.ui.dialog.view.DialogShowingView/>
  64. Bind View to it instead of observing in code </layout>

    <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3ViewModel" /> </data> <by.ve.dialogsbinding.ui.dialog.view.DialogShowingView app:dialogConfig="@{viewModel.errorDialogConfig}" app:visibleOrGone=“@{viewModel.isDialogVisible}"/>
  65. Adapt callbacks in ViewModel override fun showErrorDialog() { isDialogVisible.value =

    true } override fun hideErrorDialog() { isDialogVisible.value = false } } class Solution3ViewModel( service: FirstTryFailingService ) : BaseSolutionViewModel(service) { val isDialogVisible = MutableLiveData<Boolean>(false) val errorDialogConfig = STANDARD_DIALOG_CONFIG
  66. Adapt callbacks in ViewModel override fun showErrorDialog() { isDialogVisible.value =

    true } override fun hideErrorDialog() { isDialogVisible.value = false } } val errorDialogViewModel = DialogViewModel( positiveClick = ::onErrorRetry, negativeClick = ::onErrorCancel ) class Solution3ViewModel( service: FirstTryFailingService ) : BaseSolutionViewModel(service) { val isDialogVisible = MutableLiveData<Boolean>(false) val errorDialogConfig = STANDARD_DIALOG_CONFIG
  67. Bind View to them </layout> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data>

    <variable name="viewModel" type="by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3ViewModel" /> </data> <by.ve.dialogsbinding.ui.dialog.view.DialogShowingView app:dialogConfig="@{viewModel.errorDialogConfig}" app:visibleOrGone=“@{viewModel.isDialogVisible}"/>
  68. Bind View to them </layout> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data>

    <variable name="viewModel" type="by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3ViewModel" /> </data> <by.ve.dialogsbinding.ui.dialog.view.DialogShowingView app:dialogConfig="@{viewModel.errorDialogConfig}" app:visibleOrGone=“@{viewModel.isDialogVisible}" app:dialogViewModel="@{viewModel.errorDialogViewModel}"/>
  69. Setup binding in Activity, just like we did before Notice

    that there’s now nothing more than binding setup. Activity is very thin! class Solution3Activity : AppCompatActivity() { private val viewModel by viewModel<Solution3ViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // inflate layout and setup binding } }
  70. The "Scope" problem - Solved We don’t use EventBus anymore.

    Databinding plugin manages lifecycle for us.
  71. Solution Analysis • "Duplicated State" problem: view state is stored

    only in viewmodel. Hooray! • "Abstraction" problem: ViewModel knows nothing about view and connects with DialogShowingView via Android databinding. Hooray!
  72. Solution Analysis • "Duplicated State" problem: view state is stored

    only in viewmodel. Hooray! • "Abstraction" problem: ViewModel knows nothing about view and connects with DialogShowingView via Android databinding. Hooray! • "Scope" problem: DialogShowingView and ViewModel communicate through the databinding mechanism. Hooray!
  73. Solution Analysis • "Duplicated State" problem: view state is stored

    only in viewmodel. Hooray! • "Abstraction" problem: ViewModel knows nothing about view and connects with DialogShowingView via Android databinding. Hooray! • "Scope" problem: DialogShowingView and ViewModel communicate through the databinding mechanism. Hooray! • "Expandability" problem: switching to embed view is just change of the XML. Hooray!
  74. Solution Analysis • "Duplicated State" problem: view state is stored

    only in viewmodel. Hooray! • "Abstraction" problem: ViewModel knows nothing about view and connects with DialogShowingView via Android databinding. Hooray! • "Scope" problem: DialogShowingView and ViewModel communicate through the databinding mechanism. Hooray! • "Expandability" problem: switching to embed view is just change of the XML. Hooray! • "Expandability" problem: just like any default view DialogShowingView is being setup in XML. Hooray!
  75. If we use DialogShowingView within Fragment then, upon replacing the

    Fragment with another, alert dialog is not being dismissed. Hide the dialog in onDetachedFromWindow method of the DialogShowingView. Issue Fix
  76. If we use DialogShowingView within Fragment then, upon replacing the

    Fragment with another, alert dialog is not being dismissed. Hide the dialog in onDetachedFromWindow method of the DialogShowingView. DialogShowingView effects layout hierarchy. Override onMeasure to always return zero dimensions. Issue Fix
  77. If we use DialogShowingView within Fragment then, upon replacing the

    Fragment with another, alert dialog is not being dismissed. Hide the dialog in onDetachedFromWindow method of the DialogShowingView. DialogShowingView effects layout hierarchy. Override onMeasure to always return zero dimensions. DialogShowingView doesn't work within UI less fragment. There's no :( Issue Fix
  78. class ToastShowingView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) {

    private lateinit var toast: Toast private lateinit var binding: ViewDataBinding var bindingData: ToastViewModel? = null set(value) { // update binding } init { binding = ... toast = ... } fun show() { toast.show() } } ToastShowingView
  79. @BindingAdapter("toastDisplaySignal") fun ToastShowingView.show(signal: ToastDisplaySignal?) { signal?.let { show() } }

    @BindingAdapter("toastViewModel") fun ToastShowingView.bindViewModel(viewModel: ToastViewModel?) { if (viewModel != null) { bindingData = viewModel } } And we need some BindingAdapters
  80. @BindingAdapter("toastDisplaySignal") fun ToastShowingView.show(signal: ToastDisplaySignal?) { signal?.let { show() } }

    @BindingAdapter("toastViewModel") fun ToastShowingView.bindViewModel(viewModel: ToastViewModel?) { if (viewModel != null) { bindingData = viewModel } } And we need some BindingAdapters Wait a sec…
  81. @BindingAdapter("toastDisplaySignal") fun ToastShowingView.show(signal: ToastDisplaySignal?) { signal?.let { show() } }

    @BindingAdapter("toastViewModel") fun ToastShowingView.bindViewModel(viewModel: ToastViewModel?) { if (viewModel != null) { bindingData = viewModel } } And we need some BindingAdapters What is ToastDisplaySignal?
  82. The “Default Value” problem • SingleLiveEvent actually holds state; •

    When we use call() we set it to null; • ViewDataBinding#executeBindings method is being called for the first time earlier, then we set binding data to it;
  83. The “Default Value” problem • SingleLiveEvent actually holds state; •

    When we use call() we set it to null; • ViewDataBinding#executeBindings method is being called for the first time earlier, then we set binding data to it; • And thus it passes default value to the binding adapters. This value is null;
  84. The “State” problem • As we already know SingleLiveEvent actually

    holds state; • ViewDataBinding#executeBindings is being executed each time we get back to the first Fragment;
  85. The “State” problem • As we already know SingleLiveEvent actually

    holds state; • ViewDataBinding#executeBindings is being executed each time we get back to the first Fragment; • It gets value of SingleLiveEvent via getter;
  86. The “State” problem • As we already know SingleLiveEvent actually

    holds state; • ViewDataBinding#executeBindings is being executed each time we get back to the first Fragment; • It gets value of SingleLiveEvent via getter; • That value is not null anymore as we’ve already shown the Toast;
  87. TL;DR • In MVVM architecture dialogs, toasts, popups, etc. can

    be setup in layout.xml and managed as any other Android View;
  88. TL;DR • In MVVM architecture dialogs, toasts, popups, etc. can

    be setup in layout.xml and managed as any other Android View; • Helper views, which display them, must have zero dimensions;
  89. TL;DR • In MVVM architecture dialogs, toasts, popups, etc. can

    be setup in layout.xml and managed as any other Android View; • Helper views, which display them, must have zero dimensions; • To use SingleLiveEvent with databinding you need to "fix" it first!