setTargetFragment deprecated

Well…what do we do for this one. pg. 271 chap 13.

Thanks.

What version(s) is this deprecated in? I just implemented this chapter with the following and did not get a deprecation warning:

  • Android Studio 4.1.2
  • Kotlin 1.4.30-release-Studio-4.1-1
  • androidx.core:core-ktx:1.3.2
  • andoridx.appcompat:appcompat:1.2.0

I see other posts stating this feature is deprecated and to use a result listener in its place, but not sure when the deprecation kicks in.

Yes, I tried using a resultListener but this fix requires other changes that I don’t understand.

I am using Android Studio 4.1.2
‘androidx.appcompat:appcompat:1.3.0-beta01’
‘androidx.core:core-ktx:1.5.0-beta01’
‘androidx.fragment:fragment-ktx:1.3.0-beta01’

I see now, using

androidx.appcompat:appcompat:1.3.0-beta01

displays the deprecation. I usually don’t see the early deprecation warnings as I don’t use the alpha/beta versions and instead the most recent stable version. I’m going to work this through now to see the new implementation and will reply back to here.

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)

4 Likes

Wow! I’m going to implement and see if I can get it to work. Thanks!

I’ve been reading more about this lately and I believe where I said to use parentFragmentManager we should instead use childFragmentManager.

The parentFragmentManager would be the Fragment Manager associated with the Activity (which is our parent and manages us). The childFragmentManger is associated with us and manages our children.

If we start the dialog with the parentFragmentManager then the Activity’s Fragment Manager contains a list of transactions for us and our children. If we close, our fragment cannot be destroyed. The children we contain exist as Fragment Transactions within the Activity and remain in memory. Since our children still exist, we cannot be fully destroyed and this will lead to a memory leak.

To fix the above post (setTargetFragment deprecated - #5 by jpaone), the two places to change are:

original incorrect:

// CrimeFragment.kt
dateButton.setOnClickListener {
  DatePickerFragment
    .newInstance(crime.date, REQUEST_DATE)
    .show(parentFragmentManager, REQUEST_DATE)
}

corrected:

// CrimeFragment.kt
dateButton.setOnClickListener {
  DatePickerFragment
    .newInstance(crime.date, REQUEST_DATE)
    .show(childFragmentManager, REQUEST_DATE)
}

as well as, original incorrect

// CrimeFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  ...
  parentFragmentManager.setFragmentResultListener(REQUEST_DATE, viewLifecycleOwner, this)
}

corrected:

// CrimeFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  ...
  childFragmentManager.setFragmentResultListener(REQUEST_DATE, viewLifecycleOwner, this)
}
2 Likes

how to use(implement) this function

my brain is total frozen i can’t do I am trying for 8 hours now
any one just send me yours CrimeFragment and DatePickerFragment

Marvelous, clear explanation. Worked immediately.

I imported this:
import androidx.fragment.app.setFragmentResult

So it hasn’t worked for me to import: import androidx.fragment.app.setFragmentResult
even if I updated my Gradle with last version of androidx.fragment:fragment library

So, what have done here:

Instead of directly reference setFragmentResult I put:

parentFragmentManager.setFragmentResult(resultRequestCode, result)

So, I’m referencing parentFragmentManager in this specific case as I’m using it from DatePickerFragment which is the child of CrimeFragment. In any reference that I used from CrimeFragment to it’s child I used childrenFragmentManager.

Any news on supporting androidx packages? It does currently work with jetifier, but throws up error-level warnings in studio, which always had me checking back to classes to make sure I haven’t messed something up.

Could maybe make a new artifact with these classes to keep backwards compatibility?

The code is running but the date is not changing

What about this setTargetFragment thingy. Where you able to resolve it or did you downgrade?

Jeff Paone !!! You’re the man !! Thankyou !!!

the code run well. but the old date still the same and does not change in CrimeFragment

In order to apply changes of date button in CrimeFragment, you need to change the null value in the return statement of function onCreatedDialog() in DatePickerFragment class with dateListener.

class DatePickerFragment: DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
... 
return DatePickerDialog(
            requireContext(),
            dateListener,
            initiateYear,
            initiateMonth,
            initiateDay
        )
}