I really like the new implementation, it is much more elegant and matches the same paradigm used for Activities to communicate when passing data back & forth.
Looking back at Chapters 1-6 when working with GeoQuiz, the QuizActivity
and CheatActivity
communicated as follows:
// QuizActivity.kt
private const val REQUEST_CODE_CHEAT = 0
override fun onCreate(savedInstanceState: Bundle?) {
...
cheatButton.setOnClickListener {
...
startActivityForResult(intent, REQUEST_CODE_CHEAT)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode != Activity.RESULT_OK) return
if(requestCode == REQUEST_CODE_CHEAT) { ... }
}
// CheatActivity
private fun setAnswerShownResult(isAnswerShown: Boolean) {
val data = Intent().apply {
putExtra(EXTRA_ANSWER_SHOWN, isAnswerShown)
}
setResult(Activity.RESULT_OK, data)
}
The QuizActivity
is sending the intent to the OS Activity Manager with the associated request code. The CheatActivity
then sets the result which the Activity Manager then returns back to QuizActivity
. The Activity Manager keeps the association of where the intent came from and forwards the result to the associated activity with the request code attached. It’s possible that the calling activity (QuizActivity
) could start up one of many target activities (CheatActivity
in this case). The request code allows us to know which start request the result corresponds to so we can handle it accordingly.
Now to the Fragment situation. The latest dependency that needs to be added for this is
implementation 'androidx.fragment:fragment-ktx:1.3.0'
It just came out of beta in the last couple of days. We’ll begin on the CrimeFragment
side to create the request. With activities the request code was an integer, with fragments the request code is a string. Change the DIALOG_DATE
constant to be called REQUEST_DATE
. We will need to pass this request code to the builder for our DatePickerFragment. Listing 13.6 will now become
private const val REQUEST_DATE = "DialogDate"
dateButton.setOnClickListener {
DatePickerFragment
.newInstance(crime.date, REQUEST_DATE)
.show(parentFragmentManager, REQUEST_DATE)
}
We’ll get to why we pass the request code to the newInstance()
method in a moment. What spurred this whole thread was setTargetFragment()
being deprecated. But additionally, requireFragmentManager()
is also deprecated. We want to explicitly get the parentFragmentManager
that is hosting the CrimeFragment
and not the childFragmentManager
that would be hosting any nested fragments inside the CrimeFragment
.
On to DatePickerFragment::newInstance()
. We need to store the request code for future use when the result is sent back. Both arguments to newInstance()
will be placed on to the Bundle.
private const val ARG_DATE = "date"
private const val ARG_REQUEST_CODE = "requestCode"
fun newInstance(date: Date, requestCode: String): DatePickerFragment {
val args = Bundle().apply {
putSerializable(ARG_DATE, date)
putString(ARG_REQUEST_CODE, requestCode)
}
return DatePickerFragment().apply {
arguments = args
}
}
We’re now ready to send the result back. In the dateListener
specification, we will no longer reference the targetFragment
. We’ll now set the result for the fragment. Listing 13.9 will be updated as follows
val dateListener = DatePickerDialog.OnDateSetListener {
_: DatePicker, year: Int, month: Int, day: Int ->
val resultDate: Date = GregorianCalendar(year, month, day).time
// create our result Bundle
val result = Bundle().apply {
putSerializable(RESULT_DATE_KEY, resultDate)
}
val resultRequestCode = requireArguments().getString(ARG_REQUEST_CODE, "")
setFragmentResult(resultRequestCode, result)
}
For the above to work, we also need to make a constant RESULT_DATE_KEY
set to a string for the Bundle key. What is happening in the above? We’re creating the result Bundle (opposed to the result Intent for activities). We then send the result Bundle back to the Fragment Manager with the corresponding request code.
The Fragment Manager may be managing several fragments all at the same time. When it receives the result, it checks its set of result listeners to determine which fragment to send the result back to. It uses the request code to determine who to send the result to. This is why we had the calling fragment (CrimeFragment
) send the request code to the target fragment (DatePickerFragment
) so the result would get routed to the correct caller. To complete this message passing system, we need to have CrimeFragment
inform the Fragment Manager that it is listening for results corresponding to REQUEST_DATE
.
We’ll first make CrimeFragment
a listener for fragment results. This is accomplished by implementing the FragmentResultListener
interface.
class CrimeFragment : Fragment(), FragmentResultListener {
To be a FragmentResultListener
, the class needs to implement the onFragmentResult()
method. This method will receive the request code and the corresponding result. Here we can then handle the result appropriately. Let’s stub out the method:
override fun onFragmentResult(requestCode: String, result: Bundle) {
when(requestCode) {
REQUEST_DATE -> {
Log.d(TAG, "received result for $requestCode")
// handle result - will fill in later
}
}
}
We’ve now established ourselves as a fragment result listener. The next step is to inform the Fragment Manager that we are listening for results related to REQUEST_DATE
. We will put this registration in to the onViewCreated()
method of CrimeFragment
to ensure that our view is in a created state and has a lifecycle.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
parentFragmentManager.setFragmentResultListener(REQUEST_DATE, viewLifecycleOwner, this)
}
At this point, we can run our app and select a date. We’ll see the Log message print that the result was received. We now need to handle the result. Since DatePickerFragment
sent the result, we’ll need to ask the class to unpack the result for us and return to us its contents. In the companion object
of DatePickerFragment
create a second method to unpack the result.
fun getSelectedDate(result: Bundle) = result.getSerializable(RESULT_DATE_KEY) as Date
Now in the onFragmentResult()
method of CrimeFragment
, when we get the result we can unpack the contents and actually set the date on our crime.
override fun onFragmentResult(requestCode: String, result: Bundle) {
when(requestCode) {
REQUEST_DATE -> {
Log.d(TAG, "received result for $requestCode")
crime.date = DatePickerFragment.getSelectedDate(result)
updateUI()
}
}
}
We can now run and begin setting dates for each crime as desired.
The new implementation for Fragment communication has direct parallels to Activity communication.
// Activities // Fragments
startActivityForResult() parentFragmentManager.setFragmentResultListener()
setResult() setFragmentResult()
onActivityResult() onFragmentResult()
The difference in implementation is that the Activity class inherently is an Activity Result Listener connected with the Activity Manager, while we need to explicitly establish the Fragment as a Fragment Result Listener with the Fragment Manager. It is automatically handled for the Activity case because there will only be a single Activity running and the Activity stack manages the message passing between caller and target. Since there can be multiple Fragment running concurrently, we have to inform the Fragment Manager where to route the associated messages.
(Edit to correct spelling)