Update to androidx.fragment:fragment:1.3.0-alpha08: registerForActivityResult not allowed after onCreate anymore. How to use after onCreate?

12,794

Solution 1

It just means that you shouldn't register the callback after onCreate().

So you can do this

private val checkPermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
        ...
    }

and then launch the check anytime you need it

checkPermission.launch(array-of-permissions)

The reason:

When starting an activity for a result, it is possible (and, in cases of memory-intensive operations such as camera usage, almost certain) that your process and your activity will be destroyed due to low memory.

For this reason, the Activity Result APIs decouple the result callback from the place in your code where you launch the other activity. As the result callback needs to be available when your process and activity are recreated, the callback must be unconditionally registered every time your activity is created, even if the logic of launching the other activity only happens based on user input or other business logic.

Solution 2

I had recently run into the same problem and I created my own PermissionDSL based on the Question here which helps me separate the permission code from fragments, (Although this method is not very different from the original way of requesting permissions)

Note : This is the answer to the updated Question

... Is there a way to keep those inline functions without having to change them drastically?

  • This can be used only in fragments
  • This is for requesting single permission although it can easily be extended for multiple permissions

Step 1 Add gradle dependency

def lifecycle_version = "2.3.0"

implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

Step 2 Add the following code

inline fun <reified R : ActivityResultLauncher<String>> Fragment.requestPermission(
    permission: String,
    noinline granted: (permission: String) -> Unit = {},
    noinline denied: (permission: String) -> Unit = {},
    noinline explained: (permission: String) -> Unit = {}

): ReadOnlyProperty<Fragment, R> = PermissionResultDelegate(this, permission, granted, denied, explained)



class PermissionResultDelegate<R : ActivityResultLauncher<String>>(
    private val fragment: Fragment, private val permission: String,
    private val granted: (permission: String) -> Unit,
    private val denied: (permission: String) -> Unit,
    private val explained: (permission: String) -> Unit
) :
    ReadOnlyProperty<Fragment, R> {

    private var permissionResult: ActivityResultLauncher<String>? = null


    init {
        fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                fragment.apply {
                    permissionResult = registerForActivityResult(
                        ActivityResultContracts.RequestPermission()
                    ) { isGranted: Boolean ->

                        when {
                            isGranted -> granted(permission)
                            shouldShowRequestPermissionRationale(permission) -> denied(permission)
                            else -> explained(permission)
                        }
                    }
                }
            }

            override fun onDestroy(owner: LifecycleOwner) {
                permissionResult = null
            }
        })
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): R {
        permissionResult?.let { return (it as R) }

        error("Failed to Initialize Permission")
    }
}

Usage

class DemoFrag : Fragment {

private val readStoragePermissionResult: ActivityResultLauncher<String> by requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE,
            granted = {
                Log.d(TAG, "Granted")
            }, denied = {
        Log.d(TAG", "Denied")
    })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        readStoragePermissionResult.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
    }
}
Share:
12,794
goldensoju
Author by

goldensoju

Updated on August 19, 2022

Comments

  • goldensoju
    goldensoju over 1 year

    Initial Question (18/05/2020):

    So with the latest Update from

    • androidx.fragment:fragment:1.3.0-alpha07

    to

    • androidx.fragment:fragment:1.3.0-alpha08

    I get the error:

    FragmentXY is attempting to registerForActivityResult after being created. Fragments must call registerForActivityResult() before they are created (i.e. initialization, onAttach(), or onCreate()).

    I used to check permissions in my StartFragment (Single Activity App, in onViewCreated) after showing to the User information about the use of those permissions and why they are needed. Everything worked perfectly for the last 3(?) months.

    I see in the changelog:

    Behavior Changes

    [...]
    Calling registerForActivityResult() after onCreate() now throws an exception indicating that this is not allowed rather than silently failing to deliver results after a configuration change. (b/162255449) "

    I downgraded back to version 1.3.0-alpha07 for the moment.
    But if I need registerForActivityResult in my Fragments AFTER the view is created (e.g. for permissions), how can I do it when upgrading to version 1.3.0-alpha08?

    The docs state that I should use launch() in onCreate of my Fragment (see below) but that would mean I have to do it before the view is created, and that would be contradictory to my app flow.

    Behavior Changes

    [...]
    You can now call launch() on an ActivityResultLauncher in the onCreate() lifecycle method of a fragment. (b/161464278) "

    As this behaviour seems to be intended by the developers, it is not a bug or anything but how can I continue using ActivityResults after onCreate? Any ideas?


    Edit (19/05/2020):

    Thanks to @A.Andriyishyna I understand that registration (in onCreate) and execution (when needed, e.g. in onViewCreated) have to be handled separately. Problem is that I have handy inline functions (with courtesy to Flywith24) in other files, which help me to separate the permission BL from the View (Fragment).
    Is there a way to keep those inline functions without having to change them drastically?

    1. Fragment
    class GalleryFragment: ScopedFragment() {
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            initializePermissions(requiredContext)
        }
    
        private fun initializePermissions(context: Context) {
            storagePermissions(
                context = context,
                actionOnGranted = { showImages() },
                actionOnDeclined = { showNoAccess() },
                actionRepeat = { initializePermissions(context) }
            )
        }
    }
    
    1. PermissionDSL
    inline fun Fragment.storagePermissions(
        context: Context,
        crossinline actionOnGranted: () -> Unit,
        crossinline actionOnDeclined: () -> Unit,
        crossinline actionRepeat: () -> Unit
    ) {
        when {
            Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> {
    
                if (
                    ContextCompat.checkSelfPermission(
                        context, Manifest.permission.READ_EXTERNAL_STORAGE
                    ) == PackageManager.PERMISSION_GRANTED
                ) {
                    actionOnGranted()
                } else {
                    permission(
                        Manifest.permission.READ_EXTERNAL_STORAGE
                    ) {
                        granted = {
                            actionOnGranted()
                        }
                        denied = {
                            actionRepeat()
                        }
                        explained = {
                            actionOnDeclined()
                        }
                    }
                }
            }
    
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
                if (
                    ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_MEDIA_LOCATION
                    ) == PackageManager.PERMISSION_GRANTED) {
                    Log.d("Storage Permission", "Permission already granted.")
                    actionOnGranted()
                } else {
                    Log.d("Storage Permission", "No Permission Yet -> Ask for it!")
                    permissions(
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_MEDIA_LOCATION
                    ) {
                        allGranted = {
                            actionOnGranted()
                        }
                        denied = {
                            Log.d("Storage Permission", "Denied")
                            actionRepeat()
                        }
                        explained = {
                            Log.d("Storage Permission", "Permanently Denied")
                            actionOnDeclined()
                        }
                    }
                }
            }
        }
    }
    
    1. PermissionExtension
    inline fun Fragment.requestPermission(
        permission: String,
        crossinline granted: (permission: String) -> Unit = {},
        crossinline denied: (permission: String) -> Unit = {},
        crossinline explained: (permission: String) -> Unit = {}
    
    ) {
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
            when {
                result -> granted.invoke(permission)
                shouldShowRequestPermissionRationale(permission) -> denied.invoke(permission)
                else -> explained.invoke(permission)
            }
        }.launch(permission)
    }
    
    
    inline fun Fragment.requestMultiplePermissions(
        vararg permissions: String,
        crossinline allGranted: () -> Unit = {},
        crossinline denied: (List<String>) -> Unit = {},
        crossinline explained: (List<String>) -> Unit = {}
    ) {
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result: MutableMap<String, Boolean> ->
    
            val deniedList = result.filter { !it.value }.map { it.key }
            when {
                deniedList.isNotEmpty() -> {
    
                    val map = deniedList.groupBy { permission ->
                        if (shouldShowRequestPermissionRationale(permission)) DENIED else EXPLAINED
                    }
    
                    map[DENIED]?.let { denied.invoke(it) }
    
                    map[EXPLAINED]?.let { explained.invoke(it) }
                }
                else -> allGranted.invoke()
            }
        }.launch(permissions)
    }
    
    • Karunesh Palekar
      Karunesh Palekar almost 3 years
      Have You resolved it ? I am facing the same issue . Can you help ?
  • goldensoju
    goldensoju over 3 years
    Thanks for that clear explanation. Problem is now that I'm not sure if I can keep my inline functions where the activityresults are handled. I updated my initial question, maybe you have an idea?
  • okarakose
    okarakose over 3 years
    I need to use them as inline functions inside ViewModel. Did you find any solution ? @goldensoju
  • goldensoju
    goldensoju over 3 years
    @okarakose No I had no luck with inline functions. Actually I removed them all and have the logic now in fragment/viewmodel.
  • BabyishTank
    BabyishTank about 3 years
    I found that instaed of using the fragment's context, using AppCompatActivity context avoided this problem. ((AppCompatActivity)context).registerForActivityResult
  • Lexon Li
    Lexon Li about 2 years
    I didn't understand this answer until I realised that declaring a val in a fragment/activity will make it initialise in onCreate, thanks though!