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?