Home > Software engineering >  Callback from registerForActivityResult not called when fragment is destroyed
Callback from registerForActivityResult not called when fragment is destroyed

Time:12-07

Let's assume a fragment has this ActivityResultLauncher:

class MyFragment : Fragment(R.layout.my_fragment_layout) {

    companion object {
        private const val EXTRA_ID = "ExtraId"

        fun newInstance(id: String) = MyFragment().apply {
            arguments = putString(EXTRA_ID, id)
        }
    }

    private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            Timber.i("Callback successful")
        }
    }

...

This Fragment a wrapped in an Activity for temporary architectural reasons, it will eventually be moved into an existing coordinator pattern.

class FragmentWrapperActivity : AppCompatActivity() {
    
    private lateinit var fragment: MyFragment
    private lateinit var binding: ActivityFragmentWrapperBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFragmentWrapperBinding.inflate(this)
        setContentView(binding.root)

        fragment = MyFragment.newInstance("blah")

        supportFragmentManager.transact {
            replace(R.id.fragment_container, fragment)
        }
    }
}

And we use that launcher to start an Activity, expecting a result:

fun launchMe() {
    val intent = Intent(requireContext(), MyResultActivity::class.java)
    launcher.launch(intent)
}

On a normal device with plenty of available memory, this works fine. MyResultActivity finishes with RESULT_OK, the callback is called and I see the log line.

However, where memory is an issue and the calling fragment is destroyed, the launcher (and its callback) is destroyed along with it. Therefore when MyResultActivity finishes, a new instance of my fragment is created which is completely unaware of what's just happened. This can be reproduced by destroying activities as soon as they no longer have focus (System -> Developer options -> Don't keep activities).

My question is, if my fragment is reliant on the status of a launched activity in order to process some information, if that fragment is destroyed then how will the new instance of this fragment know where to pick up where the old fragment left off?

CodePudding user response:

It seems that if I'm launching an Activity from a wrapped fragment, the launch needs to happen from the Activity rather than the Fragment. So if I were to create a callback interface as a very basic solution:

class FragmentWrapperActivity : AppCompatActivity(), MyFragmentCallback {
    
    private lateinit var fragment: MyFragment
    private lateinit var binding: ActivityFragmentWrapperBinding

    private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            Timber.i("Callback successful")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFragmentWrapperBinding.inflate(this)
        setContentView(binding.root)

        fragment = MyFragment.newInstance("blah", this)

        supportFragmentManager.transact {
            replace(R.id.fragment_container, fragment)
        }
    }

    override fun launchMe() {
       val intent = Intent(requireContext(), MyResultActivity::class.java)
       launcher.launch(intent)
    }
}

The new interface:

interface MyFragmentCallback {
    fun launchMe()
}

And from the fragment:

class MyFragment : Fragment(R.layout.my_fragment_layout) {

    companion object {
        private const val EXTRA_ID = "ExtraId"

        fun newInstance(id: String, callback: MyFragmentCallback) = MyFragment().apply {
            arguments = putString(EXTRA_ID, id)
            setCallback(callback)
        }
    }

    private lateinit var callback: MyFragmentCallback

    // Internal for testing with FragmentScenario
    internal fun setCallback(callback: MyFragmentCallback) {
        this.callback = callback
    }

    ...

    callback.launchMe()

Not elegant, I'll probably come back to edit this answer in future with something a bit cleaner, but it solves the problem I have.

As a guess, I'd say that as I'm launching a FragmentWrapperActivity that creates its own instance of MyFragment, registerForActivityResult is using the ActivityResultRegistry from FragmentWrapperActivity. Again, I'm not 100% certain on the reason but it seems to work.

CodePudding user response:

Your minimal fragment is unconditionally replacing the existing fragment with a brand new fragment everytime it is created, thus causing the previous fragment, which has had its state restored, to be removed.

As per the Create a Fragment guide, you always need to wrap your code to create a fragment in onCreate in a check for if (savedInstanceState == null):

In the previous example, note that the fragment transaction is only created when savedInstanceState is null. This is to ensure that the fragment is added only once, when the activity is first created. When a configuration change occurs and the activity is recreated, savedInstanceState is no longer null, and the fragment does not need to be added a second time, as the fragment is automatically restored from the savedInstanceState.

So your code should actually look like:

fragment = if (savedInstanceState == null) {
    // Create a new Fragment and add it to
    // the FragmentManager
    MyFragment.newInstance("blah").also { newFragment ->
        supportFragmentManager.transact {
            replace(R.id.fragment_container, newFragment)
        }
    }
} else {
    // The fragment already exists, so
    // get it from the FragmentManager
    supportFragmentManager.findFragmentById(R.id.fragment_container) as MyFragment
}
  • Related