:android-wave: Hello everyone, how do I consume <o...
# compose-android
j
👋 Hello everyone, how do I consume onTopResumedActivityChanged lifecycle events properly in compose? Use case: If the user grants a permission in split screen mode the app state should at least get updated as soon as the user presses something in the app again.
I currently use a MutableSharedFlow. And get the activity via context. Not sure if thats the best approach.
t
Not sure if this will help. But my code for permissions looks like this:
Copy code
val permissionState = remember { MutablePermissionState(ctx, permission, launcher) }
// observe permission
val permissionCheckerObserver = remember(permissionState.hasPermission) {
    LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME) {
            permissionState.hasPermission = permission.checkPermission(ctx)
        }
    }
}
DisposableEffect(lifecycle, permissionCheckerObserver) {
    lifecycle.addObserver(permissionCheckerObserver)
    onDispose { lifecycle.removeObserver(permissionCheckerObserver) }
}
I think since compose 1.6.0 there is also a new api for lifecycle:
Copy code
val ctx = LocalContext.current
val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.RequestPermission(),
    onResult = {} // already observed by lifecycle observer
)
val permissionState = remember { MutablePermissionState(ctx, permission, launcher) }
LifecycleResumeEffect(Unit) {
    permissionState.hasPermission = permission.checkPermission(ctx)
    onPauseOrDispose {
        // Maybe nothing todo?
    }
}
Please let me know if this work in the split screen use case.
Full code looks like this:
Copy code
enum class ManifestPermission(val permission: String, val minApiLevel: Int) {
    @TargetApi(33)
    POST_NOTIFICATIONS(Manifest.permission.POST_NOTIFICATIONS, 33),
    ACCESS_FINE_LOCATION(Manifest.permission.ACCESS_FINE_LOCATION, 1),
    @TargetApi(29)
    ACTIVITY_RECOGNITION(Manifest.permission.ACTIVITY_RECOGNITION, 29)
}

/**
 * Checks if the permission is granted
 *
 * Because of PermissionRequired linter check this method must
 * be named like check|enforce....Permission otherwise it will not be recognized as permission check.
 * See: <https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/PermissionDetector.kt>
 */
fun ManifestPermission.checkPermission(ctx: Context) = if (Build.VERSION.SDK_INT >= minApiLevel) {
    ContextCompat.checkSelfPermission(ctx, permission) == PermissionChecker.PERMISSION_GRANTED
} else {
    true
}

@Composable
fun rememberPermissionState(permission: ManifestPermission): PermissionState {
    val ctx = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = {} // already observed by lifecycle observer
    )
    val permissionState = remember { MutablePermissionState(ctx, permission, launcher) }

    LifecycleResumeEffect(Unit) {
        permissionState.hasPermission = permission.checkPermission(ctx)
        onPauseOrDispose {
            // Maybe nothing todo?
        }
    }
    return permissionState
}

interface PermissionState {
    val hasPermission: Boolean
    fun launchPermissionRequest()
}

private class MutablePermissionState(
    ctx: Context,
    private val permission: ManifestPermission,
    private val launcher: ManagedActivityResultLauncher<String, Boolean>
) : PermissionState {
    override var hasPermission by mutableStateOf(permission.checkPermission(ctx))
    override fun launchPermissionRequest() = launcher.launch(permission.permission)
}
j
Thx for the detailed response. Unfortunately, I think, this has the same flaw as the Accompanist implementation I use. Observing the resumed state is not enough as in splitscreen / multi window mode the activity stays resumed and therefore the permission state does not refresh after clicking back into the app. I solved it with observing the above mentioned lifecycle, and passing a fake / toggling permission to the rememberMultiplePermissionsState call.
Copy code
val isTopResumedActivity =
        LocalContext.current.getActivity()?.topResumedActivityChangedFlow?.collectAsState(true)?.value
// We add a toggling fake permission (true/false) to force a refresh of the permission state.
// This way the user can grant a permission in split screen mode and the app gets the most recent state
// after the user selects the app again.
val permissionState =
    rememberMultiplePermissionsState(permissions = getNeededAppPermissions() + isTopResumedActivity.toString())
This does work but does not feel like the best approach. 😉
t
Maybe you should file a bugreport to the Accompanist library. They should integrate this into the lib.
Btw. on a pixel 7 phone i was not able to reproduce this. On which device do you have this problem?
j
Sorry for the late response. I initially tested on a Galaxy Z Fold3 and just retested on a Pixel 4a. Both have the same issue that in split screen the permission state of the app does not reflect the actual state as the app never actually gets resumed as it always stays resumed (this is the intended behaviour of multi window nowadays).
t
Ok thx. At the end it makes sense but for all configuration changes if they only get updated when resumed this would break. So also maybe changing the locale or s.th. like that.
j
Config changes are handled differently. They listen to the onConfigurationChanged callback of the AndroidComposeView: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1641551253056900?thread_ts=1641547722.054300&amp;cid=CJLTWPH7S instead of the onResume lifecycle event.