I have just started my first Kotlin app, and I'm aiming to learn the best practices for that language along the way (and end up with a working app of course :) ). I've come across a problem I find myself struggling with for some time: What should be the flow of an onClick event?
At first, I used the strait forward way of setting the onClick in my fragment like so: binding.image_profile_picture.setOnClickListener { onChangePhoto() }.
I went over some of the kotlin training codelabs and changed my code accordingly. One thing I understood is that it is recommended to handle events in the ViewModel and not in the fragment (codelab#3), so now my onClicks are set in the layout xml like so: android:onClick="@{() -> profileViewModel.onChangePhoto()}".
The problem with that is all of my events actually need a context as they start with some kind of dialog (like the image picker). I found this article recommending to solve this problem using an event wrapper. I read the discussion on it's implementation's Github page and decided to give it a shot (also I'm not sure I like this unnecessary ViewModel-Fragment ping-pong). I implemented aminography's OneTimeEvent and now my ViewModel looks like this:
// One time event for the fragment to listen to
private val _event = MutableLiveData<OneTimeEvent<EventType<Nothing>>>()
val event: LiveData<OneTimeEvent<EventType<Nothing>>> = _event
// Types of supported events
sealed class EventType<in T>(val func: (T) -> Task<Void>?) {
class ShowMenuEvent(func: (Context) -> Task<Void>?, val view: View) : EventType<Context>(func)
class ChangePhotoEvent(func: (Uri) -> Task<Void>?) : EventType<Uri>(func)
class EditNameEvent(func: (String) -> Task<Void>?) : EventType<String>(func)
...
}
fun onShowMenu(view: View) {
_event.value = OneTimeEvent(EventType.ShowMenuEvent(Authentication::signOut, view))
}
fun onChangePhoto() {
_event.value = OneTimeEvent(EventType.ChangePhotoEvent(Authentication::updatePhotoUrl))
}
fun onEditName() {
_event.value = OneTimeEvent(EventType.EditNameEvent(Authentication::updateDisplayName))
}
...
and my Fragment's onCreateView looks like this:
...
// Observe if an event was thrown
viewModel.event.observe(
viewLifecycleOwner, {
it.consume { event ->
when (event) {
is ProfileViewModel.EventType.ShowMenuEvent ->
showMenu(event.func, event.view)
is ProfileViewModel.EventType.EditEmailEvent ->
showEditEmailDialog(event.func)
is ProfileViewModel.EventType.ChangePhotoEvent ->
showImagePicker(event.func)
...
}
}
}
)
return binding.root
And if we stick to showImagePicker as an example, it looks like this:
private fun showImagePicker(func: (Uri) -> Task<Void>?) {
onPickedFunc = func
val intent =
Intent(
Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
)
startActivityForResult(intent, RC_PICK_IMAGE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_PICK_IMAGE && resultCode == Activity.RESULT_OK) {
data?.data?.let { onPickedFunc(it)?.withProgressBar(progress_bar) }
}
}
The reason I am passing the Authentication::updatePhotoUrl function like that instead of just calling it from the Fragment is that I want to stick to the MVVM guidelines. All the calls to FirebaseAuth API feel like a "Repository-level" to me so I handle them in my Authentication class which I don't want my Fragments to interact with directly. But this is becoming funny, as I am ending up storing those functions as members of my Fragment - and that just doesn't feel right. There has to be a neater solution than that. Please help me find it :)
Thanks! Omer