-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue/9739 domain contact form refactoring #9892
Issue/9739 domain contact form refactoring #9892
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job on the refactoring! 💯 I think it makes it so much more readable! Consider my suggestions and let me know what you think 👍
WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt
Outdated
Show resolved
Hide resolved
hideFormProgressIndicator() | ||
} | ||
private fun setupObservers() { | ||
viewModel.formProgressIndicatorVisible.observe(this, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider having all the UI state fields (not the events/dialogs) in one object. I think when you have this many fields, it might be more readable to have one object (UiModel?). It's easier to test because some of the fields might depend on each other and one object fully represents all the possible states of the UI (for example you can have a logic while building the UiModel that checks that only one progress indicator is visible). I'm not saying that there is anything wrong with your approach though 👍. Let me know what you think. Posting a change can be done like liveData.postValue(liveData.value.copy(updatedField = newValue))
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense! I tried this approach, and when updating tests I realized that I'm not really sure how to test the sequence of updates to UI state. For example, currently I can test that progress indicator was shown, then dismissed, and is currently invisible like this:
verify(formProgressIndicatorObserver, times(1)).onChanged(true)
verify(formProgressIndicatorObserver, times(1)).onChanged(false)
Assertions.assertThat(viewModel.formProgressIndicatorVisible.value).isEqualTo(false)
Do you know how to achieve similar coverage using UI state object (or is it even necessary?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If your UI state object is a data class, you can do something like verify(observer, times(1)).onChanged(stateObject.copy(progressIndicator = true)
(the times(1)
is not required btw). You have to build the complete UI State object with all the fields though.
What I usually do when I want to test a sequence of objects is that I do something like
val results = mutableListOf<UiState>()
viewModel.uiState.observeForever { if (it != null) results.add(it) }
This way the results
contain all the emitted UI states and you can assert anything you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thank you! I took your approach (storing observer results) and it's way better - lets us test a snapshot of whole UI state at each step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, that's exactly what I thought we should be doing :-)
WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt
Outdated
Show resolved
Hide resolved
domain_privacy_options_radiogroup.setOnCheckedChangeListener { _, _ -> | ||
viewModel.togglePrivacyProtection(domain_privacy_on_radio_button.isChecked) | ||
} | ||
viewModel.privacyProtectionState.removeObserver(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be maybe better to always check the current value in the observer and only update the field if the value has changed? one other option we can consider is removing the on text changed/onclick/onchecked listener before updating the value but I think the first approach is better. The thing is that I think stopping the observer is not the best approach. The LiveData advantage are the continuous updates (even though they might not be necessary in this case). It's possible we might get into a state where the LiveData doesn't match the actual Ui State.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is absolutely better! And it works really well with 2-way data binding. Without it we have a problem with the data flow loop (?) - we toggle a radio button, listener assigned to it calls ViewModel, which notifies LiveData. Observer reacts to LiveData and toggles the radio button, which triggers a listener and calls ViewModel, etc. :) I replaced OnCheckedChangeListener
with normal click listener, so it works as intended, but I still remove observer for the domainContactDetails
data to avoid this (not sure how to avoid doing this).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about this approach:
val currentModel = contactFormToDomainContactModel()
if (currentModel != domainContactModel) {
populateContactForm(domainContactModel!!)
}
I'm not 100% sure this will always work but if it doesn't, we just have to make sure the fields are the same (because they will all go through the equals method). It should work because we'll be using the same method to collect the current data and updating the viewModel.domainContactDetails
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, got it! Yes, this will work :)
builder.setTitle(R.string.domain_registration_state_picker_dialog_title) | ||
builder.setItems(states.map { it.name }.toTypedArray()) { _, which -> | ||
if (targetFragment != null && targetFragment is OnStateSelectedListener) { | ||
(targetFragment as OnStateSelectedListener).onStateSelected(states[which]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm doing something very similar right now and I think it makes sense to create the ViewModel
with the Activity
context and fetch it here and set the field directly. What do you think? This way you can avoid fragment-fragment communication and you don't need the listeners.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super clever! I checked your implementation, and it works really well (I see tons of other uses for this too!) but it needs an adapter or something a bit more dynamic than the default implementation of the list in AlerDialog - I'm not sure how to update its content (from LiveData observer) after it is shown. Do you think it's ok to access LiveData
value directly (without observer) in this case? This will make things a bit easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think your approach for the data is good 👍 . The only thing I was wondering about is the callback. Instead of casting the targetFragment
and going through the fragment to the viewModel you can just inject the ViewModel.Factory
and call directly the same ViewModel which handles the Fragment.
.../test/java/org/wordpress/android/viewmodel/domains/DomainRegistrationDetailsViewModelTest.kt
Outdated
Show resolved
Hide resolved
really good changes @khaykov 👍 let me know what you think about my suggestions! |
Hey! I'm moving this to 12.7 since 12.6 has been cut. If you want it to land 12.6, please feel free to move it back, target the release branch and ping me to build a new beta. |
Thanks for the suggestions, @planarvoid ! I learned tons of stuff from them 🙇 |
builder.show() | ||
val dialogFragment = CountryPickerDialogFragment.newInstance(countries.toCollection(ArrayList())) | ||
dialogFragment.setTargetFragment(this, 0) | ||
dialogFragment.show(fragmentManager, CountryPickerDialogFragment.TAG) | ||
} | ||
|
||
private fun showFormProgressIndicator() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: this is a detail but I think it would be fine to have only one method with a boolean parameter that would set the view to visible/gone. The parameter could be named so there is no confusion setFormProgressIndicatorVisibility(visible = ...)
|
||
var preloadStep = 0 | ||
|
||
Assertions.assertThat(uiStateResults.size).isEqualTo(5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: I'd do a static import here :-)
Assertions.assertThat(uiStateResults.size).isEqualTo(5) | ||
|
||
// initial state | ||
Assertions.assertThat(uiStateResults[preloadStep].isFormProgressIndicatorVisible).isEqualTo(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be more readable to have the uiStateResults[preloadStep]
extracted in a variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea 👍 I can get rid of the majority of comments that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks pretty awesome 💯 great job. I've left a few comments that are mostly about code style so I'm approving the code right now.
The domain registration seems to work well so feel free to once you implement or ignore my comments 👍 |
Just a friendly reminder to please make sure to manually merge |
…id into issue/9739-domain-contact-form-refactoring # Conflicts: # WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java # WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt
Generated by 🚫 dangerJS |
… UI states for better readability.
@planarvoid I implemented your suggestions, so feel free to merge if they look ok! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All looks good, thanks for the changes!
This PR is a refactoring of a domain contact form.
I extracted the majority of the logic into ViewModel and added tests.
It's my first time working on ViewModel this big so I would appreciate insight on.. anything, really!
I have been staring on this code for too long, so there is a good chance I missed something.
The PR is giant, and I'm not really sure how to make it smaller since there is a lot of things going behind the scene. Good thing half of it are tests!
To test:
Install the wasabi build.
RELEASE-NOTES.txt
.