Save 37% off PRO during our Black Friday Sale! »

How to ask permission, the clean way - Droidcon 2021

677b4a116774ecdee4b161903bdcfb28?s=47 Ronaldo Pace
October 22, 2021
86

How to ask permission, the clean way - Droidcon 2021

Presentation at Droidcon 2021 by Ronaldo Pace
How to ask permission, the clean way
Tells the approach to implement Android Runtime permissions using a "clean" approach of service/repository/viewModel by the means decoupling it from the Android UI.

677b4a116774ecdee4b161903bdcfb28?s=128

Ronaldo Pace

October 22, 2021
Tweet

Transcript

  1. How to ask permission, @ronaldopace (the clean way)

  2. Slide was added after the conference I decided I'll refactor

    my previous library "permission-bitte" into a V2 that will be the full implementation on what's on this presentation. This will be a nights and weekends deal, so might take a lil bit to complete. So if you're interested, be sure to watch or star the repo on https://github.com/budius/permission-bitte
  3. data (services) domain / business (repository) presentations (view/viewModel) The "clean"

    architecture
  4. class MyFragment { fun onSomethingClick() = when { ContextCompat.checkSelfPermission(this, Manifest.permission.REQUESTED_PERMISSION)

    == PackageManager.PERMISSION_GRANTED -> useTheApi() shouldShowRequestPermissionRationale(permissions) -> showPopUpToUser() else -> requestPermissions(this, arrayOf(Manifest.permission.REQUESTED_PERMISSION), REQUEST_CODE) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { useTheApi() } else { showPopUpToUser() } } } How to ask permission (according to the docs)
  5. How to "cleanly" ask permission (naively) class MyFragment { fun

    onSomethingClick() = when { ContextCompat.checkSelfPermission(this, Manifest.permission.REQUESTED_PERMISSION) == PackageManager.PERMISSION_GRANTED -> viewModel.useTheApi() shouldShowRequestPermissionRationale(permissions) -> showPopUpToUser() else -> requestPermissions(this, arrayOf(Manifest.permission.REQUESTED_PERMISSION), REQUEST_CODE) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { viewModel.useTheApi() } else { showPopUpToUser() } } }
  6. How to ask permission (with libraries) // delegates private val

    permissionDelegate = PermissionDelegate(this) fun onSomethingClick() { if (permissionDelegate.needsPermission(Manifest.permission.REQUESTED_PERMISSION)) permission.request(Manifest.permission.REQUESTED_PERMISSION) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) = permissionDelegate.result(requestCode, permissions, grantResults) // annotation @NeedsPermission(Manifest.permission.REQUESTED_PERMISSION) fun onSomethingElseClick() { // permission was approved } @OnPermissionResult(Manifest.permission.REQUESTED_PERMISSION) fun onRequestPermissionResult(result: Code) { // handle result here }
  7. Are they really clean, if all that logic is on

    the presentation layer? data (services) domain / business (repository) presentations (view/viewModel) (don´t forget to mention DRY)
  8. interface PermissionService { val permissions: Flow<Set<Permission>> suspend fun requestPermissions(permissions: Set<Permission>)

    } data class Permission(val name: String, state: PermissionState) enum class PermissionState { GRANTED, REQUEST_PERMISSION, DENIED, SHOW_RATIONALE } PermissionService.kt data (services) domain / business (repository) presentations (view/viewModel)
  9. Dividing the problem: List<Permission> Just copy from: https://github.com/budius/permission-bitte class PermissionServiceImpl(app:

    Application) : PermissionService { private val _permissions = MutableStateFlow(extractPermissionsFromManifest(app)) override val permissions = _permissions.map{ mapToSet(it) }.distinctUntilChanged() } private fun extractPermissionsFromManifest(context: Context): Map<String, PermissionState> { val pm = context.packageManager val info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS) val names = info.requestedPermissions val groups = names.map { pm.getPermissionInfo(name, 0).group } val flags = info.requestedPermissionsFlags // parse name, group, flags into Map<String, PermissionState> // but DENIED can´t be obtained here =( map[name] = GRANTED / REQUEST_PERMISSION / SHOW_RATIONALE
  10. Dividing the problem: Updating from outside the app class PermissionServiceImpl(app:

    Application) : PermissionService { private val activityCallback = object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { val newData = extractPermissionsFromManifest(activity) _permissions.value = updatePermissions(newData, _permissions.value) } } } private fun updatePermissions( newData: Map<String, PermissionState>, current: Map<String, PermissionState> ): Map<String, PermissionState> { // any permissions that current is DENIED, // must stay DENIED // the others, pick from the newData }
  11. class PermissionServiceImpl { private var pendingResult: CompletableDeferred<RequestResult>? = null override

    suspend fun requestPermissions(permissions: Set<Permission>) { val request = filter(permissions) ?: return pendingResult = CompletableDeferred().also { requestPermissionInternal(permissions, it) // magic ? } val result = pendingResult.await() val newData: Map<String, PermissionState> = parseResult(result) _permissions.value = updatePermissions(newData, _permissions.value) } } private data class RequestResult(val permissions: Array<String>, val grantResults: IntArray) Dividing the problem: Prepare the request
  12. class PermissionFragment : Fragment() { private var pendingResult: CompletableDeferred<RequestResult>? =

    null override fun onResume() { super.onResume() requestPermissions(requireArguments().getStringArray("permissions")!!, 42) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { pendingResult?.let { it.complete("") pendingResult = null } } fun setPendingResult(pendingResult: CompletableDeferred<RequestResult>) { this.pendingResult = pendingResult } } Dividing the problem: Request permission
  13. Dividing the problem: Triggering the request class PermissionServiceImpl { private

    var pendingResult: CompletableDeferred<RequestResult>? = null private lateinit var currentActivity: WeakReferenced<FragmentActivity> override fun onActivityCreated(activity: Activity) { pendingRequest?.let { addPermissionFragment(activity, pendingResult) } } override fun onActivityStarted(activity: Activity) { if (activity is FragmentActivity) currentActivity = WeakReferenced(activity) } private fun requestPermissionInternal(permissions: Set<Permission>, pendingResult: CompletableDeferred<RequestResult>) { currentActivity.get()?.supportFragmentManager.beginTransaction() .add(PermissionFragment().apply { arguments = Bundle().apply { putStringArray("permissions", val) } setPendingResult(pendingResult)}, "permission-fragment").commit() } }
  14. Is it clean now? data (services) domain / business (repository)

    presentations (view/viewModel) PermissionFragment PermissonServiceImpl PermissionService PermissionRepo LocationRepo Onboarding V-VM SomeMapThing V-VM Google Play Services - Location
  15. Questions? @ronaldopace