I want to display a custom dialog on my app that has an input field. I extended the DialogFragment class and setup the layout I wanted. The problem I'm having is that I need a way to retrieve the data from the Input if the user hits save.
First attempt: retrieving via onAttachFragment
I first tried to create an interface with a single function that returns that value, made my fragment (the one that is going to call the Custom Dialog) implement it. Them, I overrode the onAttachFragment on my Custom Dialog like this:
class CustomDialogFragment private constructor() : DialogFragment() {
companion object {
fun getInstance(callback: Callback): CustomDialogFragment {
return Bundle().apply {
putSerializable("callback", callback)
}.let {
CustomDialogFragment().apply { arguments = it }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
saveCallback = it.getSerializable("callback")!! as Callback
}
}
private lateinit var saveCallback: Callback
interface Callback : Serializable {
fun setText(text: String)
}
private var _binding: FragmentCustomDialogBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCustomDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
private fun setupViews() = binding.run {
closeIcon.setOnClickListener { closeDialog() }
cancelButton.setOnClickListener { closeDialog() }
saveButton.setOnClickListener { onSaveClicked() }
}
private fun onSaveClicked() = binding.run {
val strMaxPrice = maxPriceInput.text.toString()
saveCallback.setText(strMaxPrice)
closeDialog()
}
override fun onAttachFragment(childFragment: Fragment) {
super.onAttachFragment(childFragment)
saveCallback = childFragment as Callback
}
private fun closeDialog() {
requireDialog().hide()
}
}
And on the fragment that displays this dialog:
class FirstFragment : Fragment(), Callback {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonFirst.setOnClickListener {
val fragmentManager = requireActivity().supportFragmentManager
val newFragment = CustomDialogFragment.getInstance(callback)
newFragment.show(fragmentManager, "dialog")
}
}
override fun setText(text: String) {
Toast.makeText(requireContext(), text, Toast.LENGTH_LONG).show()
}
...
}
However, this is not working because the onAttachFragment method is not being called.
Also, this method is deprecated, so... Let's try another way.
Second attempt: passing the callback via getInstance()
I remember that whenever you pass an argument to a FragmentDialog, you should put this value in the bundle of the Dialog so when things like configuration changes happen, your value will not be lost. However, an interface can't be saved on a bundle, but a serializable value can. So let's create an interface that implements this method:
companion object {
fun getInstance(callback: Callback): CustomDialogFragment {
return Bundle().apply {
putSerializable("callback", callback)
}.let {
CustomDialogFragment().apply { arguments = it }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
saveCallback = it.getSerializable("callback")!! as Callback
}
}
private lateinit var saveCallback: Callback
interface Callback : Serializable {
fun setText(text: String)
}
And on the fragment that is going to show this dialog:
class FirstFragment : Fragment(), CustomDialogFragment.Callback {
override fun setText(text: String) {
Toast.makeText(requireContext(), text, Toast.LENGTH_LONG).show()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonFirst.setOnClickListener {
val fragmentManager = requireActivity().supportFragmentManager
val newFragment = CustomDialogFragment.getInstance(this)
newFragment.show(fragmentManager, "dialog")
}
}
...
}
However this sounded promising and worked (I could call the Dialog, display it and retrieve the value), if I minimize my app after displaying the dialog (before or after closing the dialog), my app crashes with a BadParcelableException:
Fatal Exception: android.os.BadParcelableException
Parcelable encountered IOException writing serializable object (name = com.package.app.MyFragment)
So, that did not work too...
Third attempt: Using navigation and safeargs
I remember that we now have navigation, so let's try with this:
My nav graph:
<fragment
android:id="@+id/fragment"
android:name="com.package.app.MyFragment"
android:label="@string/my_fragment_title"
tools:layout="@layout/fragment_my">
<action
android:id="@+id/to_interestByPriceDialogFragment"
app:destination="@id/interestByPriceDialogFragment" />
</fragment>
<dialog
android:id="@+id/customDialogFragment"
android:name="com.package.app.CustomDialogFragment">
<argument
android:name="block"
app:argType="com.package.app.Callback" />
</dialog>
My CustomFragment:
class CustomDialogFragment : DialogFragment() {
private val args: CustomDialogFragmentArgs by navArgs()
private val block = args.block
My activity:
findNavController().navigate(MyFragmentDirections.toCustomDialogFragment(object : Callback {
override fun setText(text: String) {
Toast.makeText(requireContext(), text, Toast.LENGTH_LONG).show()
}
}))
The callback:
interface Callback : java.io.Serializable {
fun setText(text: Int)
}
That also did not work, displaying this error:
Caused by: java.lang.IllegalStateException: Fragment CustomDialogFragment{7fd896f} (a78e55c0-62f1-48c2-bc19-3f9d6f85adb2) has null arguments
I know one way to "solve the problem", which would be passing the callback as a lambda via the CustomDialog constructor, but I'm not sure if it's a good practice.
Any idea on how to solve this properly?
Well, I'm not sure how all the cool kids are doing it, but let me explain how I chose to do it and the reasons why:
How I'm doing it:
The fragment dialog class:
The fragment calling the Dialog:
Why I'm doing this way?
On a process death, android will kill my Dialog and try to recreate it. In this process, android will try and call a default constructor that does not receive any parameter. So, passing anything to a constructor is not an option if you want to handle this scenario.
He also saves the bundle of the fragment that was destroyed, so it can be used in the next fragment.
Since I have to store this value in the Bundle and it's limited to a few types (primitives, parcelable, serializable...) a function is not an option. So, function is also not something we can use.
Serializable is slower indeed, but If you try to implement Parcelable into an interface (i.e.
interface Callback : Parcelable), it will force whoever implements this interface to implement[describeContents][7]and[writeToParcel][7], since you can't use @Parcelize anotation to make it cleaner, once it requires the construct type to be a concrete class (not an abstract class, neither an interface). The boilerplate code makes me prefere Serializable.The SAM conversion will not work here because the interface implements serializable and as far as I know, there's no way to pass a lambda.
No, that does not work as well. If you try to do this:
Chat GPT gave me a tip to use this. I'm really not familiar to kotlin.jvm.functions, but I was not able to make it work as well.
So, in sumary, the winner is Serializable interface with a function as a callback.
But if anyone has a better option, just post a comment and I can edit my answer, or post another answer :)