Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)
}
like image 225
goldensoju Avatar asked Sep 14 '20 06:09

goldensoju


2 Answers

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.

like image 77
Oleksandra Avatar answered Nov 18 '22 20:11

Oleksandra


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)
    }
}
like image 35
vishwa raghavendra Avatar answered Nov 18 '22 21:11

vishwa raghavendra