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.
8. –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.”
13. 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();
14. 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;
15. 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.
30. 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)
}
}
31. And navigator for it
class DialogNavigator(private val fragmentManager: FragmentManager) {
}
32. And navigator for it
class DialogNavigator(private val fragmentManager: FragmentManager) {
private fun FragmentManager.isFragmentNotExist(tag: String) =
findFragmentByTag(tag) == null
}
33. 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
}
34. 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
}
38. 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)
}
39. 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)
}
40. Observe it in Activity
}
class Solution1Activity : AppCompatActivity() {
private val dialogNavigator: DialogNavigator by inject { parametersOf(this) }
private val viewModel by viewModel<Solution1ViewModel>()
41. 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)
}
}
42. 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)
}
}
44. Use EventBus: post in CommonDialogFragment
}
class CommonDialogFragment : DialogFragment() {
private val dialogEventBus by inject<EventBus>()
private val viewModel by viewModel<DialogViewModel>()
45. 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!!))
}
}
46. And observe in ViewModel
}
class Solution1ViewModel(
service: FirstTryFailingService,
private val dialogEventBus: EventBus
) : BaseSolutionViewModel(service) {
47. 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)
}
48. 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)
}
49. Activity/Fragment
Show dialog event Add DialogFragment Add DialogFragment
ViewModel
Assumesthatdialogvisible
Process killed and restored
FragmentManager
DialogFragmentstoredinthemanager
DialogFragment
Visibletotheuser
The "Duplicated State" problem
50. 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.
51. 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.
56. Solution Analysis
• "Duplicated State" problem: both ViewModel and FragmentManager control appearance of the
CommonDialogFragment;
57. 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;
58. 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;
59. 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!
60. 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 :(
66. 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)
}
}
68. Replace SingleLiveEvent with LiveData
}
class Solution2ViewModel(
service: FirstTryFailingService
) : BaseSolutionViewModel(service) {
69. Replace SingleLiveEvent with LiveData
}
class Solution2ViewModel(
service: FirstTryFailingService
) : BaseSolutionViewModel(service) {
Notice that if has initial state
val errorDialogConfig = STANDARD_DIALOG_CONFIG
70. 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
71. 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
72. Observe it in ErrorView
}
class ErrorView(
private val lifecycleOwner: LifecycleOwner,
private val dialogNavigator: DialogNavigator,
private val dialogEventBus: EventBus
) : DefaultLifecycleObserver {
73. 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
}
74. 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
}
76. Observe events in ErrorView
}
class ErrorView(
private val lifecycleOwner: LifecycleOwner,
private val dialogNavigator: DialogNavigator,
private val dialogEventBus: EventBus
) : DefaultLifecycleObserver {
77. 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() }
}
78. 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() }
}
79. Finally bind ErrorView to ViewModel in Activity
}
class Solution2Activity : AppCompatActivity() {
private val errorView: ErrorView by inject { parametersOf(this) }
private val viewModel by viewModel<Solution2ViewModel>()
80. 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
}
81. Activity/Fragment
Dialog visible state Add DialogFragment Add DialogFragment
Process killed and restored
FragmentManager
DialogFragmentinthemanager
DialogFragment
Visibletotheuser
The "Duplicated State" problem - Solved
ViewModel
LiveDatastores"false"LiveDatastores"true"
NoFragment
Notvisible
Dialog invisible state Remove DialogFragment Remove DialogFragment
82. 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.
84. 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;
85. 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;
86. 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! :(
87. 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 :(
96. 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
}
}
97. 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">
98. 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/>
99. 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}"/>
101. 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
102. 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
103. 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}"/>
104. 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}"/>
105. 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
}
}
106. The "Scope" problem - Solved
We don’t use EventBus anymore. Databinding plugin manages lifecycle for
us.
107. The "Expandability" problem - Solved
R.layout.activity_solution3_embed.xml
R.layout.activity_solution3_dialog.xml
109. 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!
110. 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!
111. 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!
112. 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!
115. 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
116. 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
117. 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
127. The “Default Value” problem
• SingleLiveEvent actually holds state;
• When we use call() we set it to null;
128. 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;
129. 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;
133. 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;
134. 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;
135. 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;
136. The “State” problem
Make getter of SingleLiveEvent#value return stored
value just once per value set
137. TL;DR
• In MVVM architecture dialogs, toasts, popups, etc. can be setup in
layout.xml and managed as any other Android View;
138. 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;
139. 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!