Creating Reusable Dialog In MVVM | Part 1 : Sharing Data | Android
Reuse Code, Reduce Bug
In this article, we will create a dialog that will be used on more than one screen (screen independent). The dialog will have these features:
- The dialog will need some data that will be shown in the dialog.
- It will contain a button, the click event of which will be handled by the parent (activity or fragment) that created it.
- The dialog should also survive configuration changes and the data should not lost.
This type of dialog is common in filters when you are showing a list of items (data) in the dialog and you want the user to choose an item (click event). Also, you want the design of the dialog to be consistent throughout the app.
Here we will be using the shared ViewModel technique but in a more abstract form so that the dialog will be independent of the parent (Fragment or Activity). For this tutorial, we will be using BottomSheetDialogFragment
as the dialog. You could also use DialogFragment
as per your requirement.
Project Setup
I have created a starter android project for the this tutorial. You can find it in the below repository.
After cloning, switch to project_setup
branch. Here is an overview of the project:
data
: This package containsAppRepository
which has two lists (authors and books).ui
: It contains UI related classes like activity and the dialog. This app has two activities (BooksActivity
andAuthorsActivity
) which lets the user choose a Book and Author respectively from their list. It usesListBottomSheet
Dialog to display the list.ListBottomSheet
hasTextView
(for the title) and aRecyclerView
to show a list usingListBottomSheetAdapter
. The adapter takes a list of String and a function (that will be called, when an item is clicked, with the item position) in the constructor.
Both Activity has a Choose button which shows ListBottomSheet
. Initially, when you click on the choose button, the sheet will show nothing in the RecyclerView
. Here is what the project_startup
branch looks like:
High-Level Design
Below is the HLD of the Reusable Dialog that we are going to implement.
Sharing List and Title
- Add the following dependencies in
build.gradle
(app module).
dependencies {
def activity_version = "1.7.2"
def fragment_version = "1.6.1"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
2. Create an interface ListBottomSheetViewModel
. It will be used by the bottom sheet as a data source.
interface ListBottomSheetViewModel {
//we are just showing String in the RecyclerView
//You can use a data class according to the list item UI.
//For eg. data class ListItem(img : Uri?, title : String) if you are showing
//title with an optional image.
val list : List<String>
}
3. Make ListBottomSheet
an abstract class and modify it to use ViewModel
as a data source for complex types (here it is a list of String)
//Make it abstract, we will make a subclass where we will define how to get these args
//also, it tells we should not create an instance of ListBottomSheet directly
abstract class ListBottomSheet(
title : String,
lazyViewModel : Fragment.() -> Lazy<ListBottomSheetViewModel>
) : BottomSheetDialogFragment() {
init {
//title is not a complex type, so we can store it in the arguments
arguments = Bundle().apply {
putString(TITLE_KEY, title)
}
}
//using Kotlin delegate property
private val viewModel by lazyViewModel()
private val adapter by lazy {
//populate adapter with viewmodel list
ListBottomSheetAdapter(viewModel.list){}
}
//get title from saved args
private val title
get() = arguments?.getString(TITLE_KEY) ?: ""
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
//adapter is first used here
//so it will be initialize here and thus viewModel
rvList.adapter = adapter
//set the title
dialogTitle.text = title
}
}
private const val TITLE_KEY = "list_bottom_sheet_title"
Let’s understand lazyViewModel : Fragment.() -> Lazy<ListBottomSheetViewModel>
.
lazyViewModel
is an extension function type with a receiver type Fragment
that returns a Lazy<ListBottomSheetViewModel>
. In simple terms, we are saying that lazyViewModel
function will be called inside a Fragment
. Thus we can call any Fragment
function inside that lambda.
It returns Lazy<ListBottomSheetViewModel>
so that ViewModel
will be initialized when we first use it. Here, we are using viewModel
in the lazy block of the adapter so that viewModel
and adapter
will be created when the adapter is first accessed (in onViewCreated).
4. Create ViewModel
s
for both activities and implements ListBottomSheetViewModel
on both of them.
class BooksViewModel : ViewModel(), ListBottomSheetViewModel{
private val bookList = AppRepository.books
override val list: List<String> = bookList.map {
//map the list to bottom sheet list type
"${it.name} by ${it.author}"
}
}
class AuthorsViewModel : ViewModel(), ListBottomSheetViewModel{
private val authorList = AppRepository.authors
override val list: List<String> = authorList.map {
it.name
}
}
5. Create a subclass of ListBottomSheet
and show it instead. The sub-class will define how to create ListBottomSheetViewModel
.
class BooksActivity : AppCompatActivity() {
//we will use BookListBottomSheet (subclass of ListBottomSheet)
private val listBottomSheet = BookListBottomSheet()
private val viewModel : BooksViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
findViewById<Button>(R.id.choose_btn).setOnClickListener {
//if the parent is fragment : use childFragmentManager
listBottomSheet.show(supportFragmentManager,"Books List")
}
}
//Creae a subclass of ListBottomSheet
class BookListBottomSheet: ListBottomSheet(
title = "Choose Book",
//if the parent is a fragment : use requireParentFragment()
lazyViewModel = {
//here this is Fragment
this.viewModels<BooksViewModel>({ ownerProducer = requireActivity() })
}
)
}
Similarly in AuthorsActivity
class AuthorsActivity : AppCompatActivity() {
private val listBottomSheet = AuthorListBottomSheet()
private val viewModel : AuthorsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
findViewById<Button>(R.id.choose_btn).setOnClickListener {
//if the parent is fragment : use childFragmentManager
listBottomSheet.show(supportFragmentManager,"Authors List")
}
}
class AuthorListBottomSheet: ListBottomSheet(
title = "Choose Author",
//if the parent is a fragment : use requireParentFragment()
lazyViewModel = { viewModels<AuthorsViewModel>({ requireActivity() }) }
)
}
viewModels
is an extension function to the Fragment in Android that returns a Lazy ViewModel.
It has a parameter ownerProducer
where we tell it to retrieve the ViewModel
using the parent activity’s scope.
If you are showing ListBottomSheet
in a Fragment (parent) instead, just replace supportFragmentManager
with childFragmentManager
and requireActivity()
with requireParentFragment()
.
Learn more about it here.
We are not creating anonymous object of
ListBottomSheet
because to recreate it on configuration change,FragmentManager
needs public static subclass ofFragment
and thus will throw an Exception when we call listBottomSheet.show.
That’s it now we can see a list of Books and Authors in the bottom sheet when we click on the respective activities choose button.
Summary
That’s it, now we can use our bottom sheet anywhere in the app where we want to show a list of items. You can customize it further to support a Flow<Response>
to handle Loading, Success, and Error state. The subclassing also enables us to modify some of the UI elements (like textSize
of TextView
title). Just we have to create an open val
or abstract fun
for that and the subclass will give the implementation.
You can find the code of this tutorial in the share_list
branch in the below GitHub repository.
In the next part, we will discuss how to handle click events and a crash that happens when dismissing dialog after rotation.
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!