Creating Reusable Dialog In MVVM | Part 2 -Sending Event| Android

Reuse Code, Reduce Bug

Prem Thakur
5 min readOct 27, 2023

This is the second part of the series of creating reusable dialog. In the previous article, we learned, how to share data between parent (activity or fragment) and dialog in MVVM. In this article, we will send events (item selection in the list) from dialog to the parent and handle it as well.

We will start from where we left off in the previous article. Clone and checkout branch share_list which contains the code of the previous article.

High-Level Design

Below is the HLD of the Reusable Dialog we are implementing.

Handling Item Click

  1. Define a function in ListBottomSheetViewModel which will receive the position of the clicked item.
interface ListBottomSheetViewModel {
//invoked with the item clicked position
fun onListItemClick(pos: Int)
}

2. The adapter in the dialog takes a function in its constructor which is called when the user clicks on an item. So we will pass the viewModel.onListItemClick in the adapter’s constructor.

    //in ListBottomSheet 
private val adapter by lazy {
ListBottomSheetAdapter(viewModel.list,viewModel::onListItemClick)
}

3. We will use SharedFlow to notify the parent (activity) when an item is clicked with the selected item. Here we’ll only do it in BooksActivity . It will be the same in the AuthorsActivity.

class BooksViewModel : ViewModel(), ListBottomSheetViewModel{

//activity will subscrive to recieve event (selected book)
private val _bookSelectEvent = MutableSharedFlow<Book>()
val bookSelectEvent = _bookSelectEvent.asSharedFlow()

override fun onListItemClick(pos: Int) {
viewModelScope.launch {
//send selected book to the activity
//or consume it if you don't want
//the activity to handle event
_bookSelectEvent.emit(bookList[pos])
}
}

}

Here we are using SharedFlow to send the one-time event to the activity. If you want to persist the selected item through the activity lifecycle, you can use StateFlow.

4. Handle event in BooksActivity.

//in OnCreateView
lifecycleScope.launch {
viewModel.bookSelectEvent.collectLatest {
//display the selected book
findViewById<TextView>(R.id.selected_item).text = "You have selected ${it.name} by ${it.author}"

//dissmiss the sheet after selection
listBottomSheet.dismiss()
}
}

Here we are showing the selected book on the screen. After that, dismiss the dialog. If you are dismissing the dialog every time after handling the click event, you can move this dismiss logic to the ListBottomSheet like this:

private val adapter by lazy {
ListBottomSheetAdapter(viewModel.list){
viewModel.onListItemClick(it)
//dismiss after selection
dismiss()
}
}

Resolving a crash

All things look perfectly fine but there is a catch. If you open the sheet, rotate the device while it is opened, and then select an item from the sheet, it will crash. See below:

Why it happened?

The thing is after rotation, the fragment manager shows the old instance of the DialogFragmenthowever, we are dismissing a new instance of the DialogFragment.

class BooksActivity : AppCompatActivity() {
//we are creating new instance every time
//activity is created
private val listBottomSheet = BookListBottomSheet()

override fun onCreate(savedInstanceState: Bundle?) {

lifecycleScope.launch {
viewModel.bookSelectEvent.collectLatest {

findViewById<TextView>(R.id.selected_item).text =
"You have selected ${it.name} by ${it.author}"

//after roation: this.listBottomSheet is not being shown
//and dismissing it without being shown causes crash
listBottomSheet.dismiss()
}
}
}

Solution

While creating the bottom sheet instance, we have to check if the FragmentManger has an instance of the bottom sheet. If the FragmentManager has the instance, we will use it else create a new instance. We can check it by using the string tag that we give when we show the sheet.

//create bottom sheet like this
private val listBottomSheet by lazy{
//if the dialog is already present in the fragment manager
//then return that dialog
supportFragmentManager.findFragmentByTag(BOOKS_BOTTOM_SHEET_TAG) as? BookListBottomSheet
//else create new instance
?: BookListBottomSheet()

//the above code is equivalent to this
//val sheet = supportFragmentManager.findFragmentByTag(BOOKS_BOTTOM_SHEET_TAG)
//return@lazy if(sheet is BookListBottomSheet) sheet else BookListBottomSheet()
}

private const val BOOKS_BOTTOM_SHEET_TAG = "books_bottom_sheet"

We are using as? here Safe (nullable) cast operator, which returns null on failure, with the conjunction of ?: Elvis operator. If the cast fails, it will return null which will be handled by the Elvis operator and thus creates a new instance of the BookListBottomSheet.

While dismissing it, we have to check if it is showing then only dismiss it.

if(listBottomSheet.dialog?.isShowing == true)
listBottomSheet.dismiss()

Extracting them into extension functions

I guess we can extract them as an extension function to remove boilerplates because wherever we are gonna use reusable dialog, we will use the same pattern to show them.

  • Lazy Dialog initialization
inline fun <reified T : DialogFragment> FragmentActivity.lazyDialogFragment(
tag : String,
crossinline dialogFactory : () -> T
) = lazy {
//if the dialog is already present in the fragment manager
//then return that dialog
//else use the dialogFactory to create new instance
supportFragmentManager.findFragmentByTag(tag) as? T ?: dialogFactory()
}

//In Activity, we will initialize dialog fragment like this
private val listBottomSheet by lazyDialogFragment(BOOKS_BOTTOM_SHEET_TAG){
BookListBottomSheet()
}
  • Dismiss if showing
fun Dialog?.dismissIfShowing(){
if(this != null && isShowing)
dismiss()
}

//when dismissing
listBottomSheet.dialog.dismissIfShowing()

Summary

So, here we finally finish our reusable dialog construction. You can find the full code of this article in the fix_rotation_crash branch of the following repository :

Thank you for joining me on this journey of Android development. For more insights, tips, and in-depth articles, feel free to connect with me on LinkedIn and Medium. If you have any questions or would like to share your thoughts, please don’t hesitate to leave a comment below. Your engagement and curiosity are greatly appreciated. Until next time, happy coding!

--

--