Корутины и управление разрешениями в Android

Из этой статьи вы узнаете, как обрабатывать разрешения среды выполнения Android, появившиеся в Android Marshmallow, с помощью корутин (сопрограмм). Такой подход позволит обрабатывать разрешения в компонентах Android с минимальным количеством кода. Вам больше не нужно будет иметь дело с функциями обратного вызова и onResult.

Обзор

Разберем пример с достаточно несложным процессом. Нам нужно создать базовый фрагмент для обработки всех необходимых разрешений в системе. Таким образом, основная часть будет изолирована от остального кода, а побочным преимуществом станет возможность повторного использования.

Затем нужно объявить этот фрагмент как абстрактный класс, чтобы дать ему расширяемость. После этого создадим еще один фрагмент, который и будет расширением базового. Здесь мы воспользуемся сопрограммами для наблюдения за состоянием разрешений в базе и обновления этого состояния на источнике вызова.

Приступим

Чтобы сделать поток состояний четче, необходимо создать запечатанный класс и включить туда все возможные обратные вызовы разрешений из системы, а также необходимую информацию, такую как код результата.

Как правило, существует четыре типа результатов запроса разрешения:

  • Предоставлено (Granted).
  • Отказано (Denied).
  • Показана причина/обоснование (Show a rational message).
  • Отказано навсегда (Permanently denied).

Взгляните на изолированный класс, который охватывает все типы результатов:

sealed class PermissionResult(val requestCode: Int) {
    class PermissionGranted(requestCode: Int) : PermissionResult(requestCode)
    class PermissionDenied(
        requestCode: Int,
        val deniedPermissions: List<String>
    ) : PermissionResult(requestCode)

    class ShowRational(requestCode: Int) : PermissionResult(requestCode)
    class PermissionDeniedPermanently(
        requestCode: Int,
        val permanentlyDeniedPermissions: List<String>
    ) : PermissionResult(requestCode)
}

BasePermissionController

В рамках этого плана мы создадим абстрактный класс с именем BasePermissionController и расширим его с помощью Fragment.

abstract class BasePermissionController : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }
}

Дальше нужно создать абстрактную функцию, которую можно импортировать во фрагменты, расширяющие основной класс, для быстрой передачи результата. Вот она:

protected abstract fun onPermissionResult(permissionResult: PermissionResult)

Потом необходимо создать функцию, которая обрабатывает взаимодействие с системой в таких случаях, как показ пользователю сообщения с причиной. Затем мы должны преобразовать данные для нашего изолированного класса. Без лишних описаний, просто посмотрите на код:

private val rationalRequest = mutableMapOf<Int, Boolean>()

protected fun requestPermissions(requestId: Int, vararg permissions: String) {
    rationalRequest[requestId]?.let {
        requestPermissions(permissions, requestId)
        rationalRequest.remove(requestId)
        return
    }
    val notGranted = permissions.filter {
        ContextCompat.checkSelfPermission(
            requireActivity(),
            it
        ) != PackageManager.PERMISSION_GRANTED
    }.toTypedArray()
    when {
        notGranted.isEmpty() ->
            onPermissionResult(PermissionResult.PermissionGranted(requestId))
        notGranted.any { shouldShowRequestPermissionRationale(it) } -> {
            rationalRequest[requestId] = true
            onPermissionResult(PermissionResult.ShowRational(requestId))
        }
        else -> {
            requestPermissions(notGranted, requestId)
        }
    }
}

Мы сделали кое-что довольно простое: во-первых, сохранили хэш-карту hashmap запросов, которые нужно выполнить, с кодом результата в качестве ключа. Затем, проверяем, предоставлены ли уже запрошенные разрешения или мы должны показать сообщение с обоснованием. Если да, то создаем объект изолированного класса с соответствующим типом и передаем обратно.

На другой стороне, где и запрашивается доступ, мы добавили код результата на карту и снова вызвали функцию. В то время как код выполняется второй раз (поскольку уже существует в карте), поток входит в блок индекса hashmap и запускает фактическое выполнение разрешений.

После взаимодействия пользователя с диалогом разрешений выводится результат onRequestPermissionsResult. Дальше создаем новый экземпляр изолированного класса, чтобы передать данные обратно на место вызова. Вот так:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    if (grantResults.isNotEmpty() &&
        grantResults.all { it == PackageManager.PERMISSION_GRANTED }
    ) {
        onPermissionResult(PermissionResult.PermissionGranted(requestCode))
    } else if (permissions.any { shouldShowRequestPermissionRationale(it) }) {
        onPermissionResult(
            PermissionResult.PermissionDenied(requestCode,
                permissions.filterIndexed { index, _ ->
                    grantResults[index] == PackageManager.PERMISSION_DENIED
                }
            )
        )
    } else {
        onPermissionResult(
            PermissionResult.PermissionDeniedPermanently(requestCode,
                permissions.filterIndexed { index, _ ->
                    grantResults[index] == PackageManager.PERMISSION_DENIED
                }
            ))
    }
}

С обработкой основных разрешений закончено. Взгляните, как будет выглядеть основной класс, когда все части собраны вместе:

abstract class BasePermissionController : Fragment() {

    private val rationalRequest = mutableMapOf<Int, Boolean>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }


    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (grantResults.isNotEmpty() &&
            grantResults.all { it == PackageManager.PERMISSION_GRANTED }
        ) {
            onPermissionResult(PermissionResult.PermissionGranted(requestCode))
        } else if (permissions.any { shouldShowRequestPermissionRationale(it) }) {
            onPermissionResult(
                PermissionResult.PermissionDenied(requestCode,
                    permissions.filterIndexed { index, _ ->
                        grantResults[index] == PackageManager.PERMISSION_DENIED
                    }
                )
            )
        } else {
            onPermissionResult(
                PermissionResult.PermissionDeniedPermanently(requestCode,
                    permissions.filterIndexed { index, _ ->
                        grantResults[index] == PackageManager.PERMISSION_DENIED
                    }
                ))
        }
    }

    protected fun requestPermissions(requestId: Int, vararg permissions: String) {

        rationalRequest[requestId]?.let {
            requestPermissions(permissions, requestId)
            rationalRequest.remove(requestId)
            return
        }

        val notGranted = permissions.filter {
            ContextCompat.checkSelfPermission(
                requireActivity(),
                it
            ) != PackageManager.PERMISSION_GRANTED
        }.toTypedArray()

        when {
            notGranted.isEmpty() ->
                onPermissionResult(PermissionResult.PermissionGranted(requestId))
            notGranted.any { shouldShowRequestPermissionRationale(it) } -> {
                rationalRequest[requestId] = true
                onPermissionResult(PermissionResult.ShowRational(requestId))
            }
            else -> {
                requestPermissions(notGranted, requestId)
            }
        }
    }

    protected abstract fun onPermissionResult(permissionResult: PermissionResult)
}

PermissionController

Затем нужно создать еще один класс с именем PermissionController и расширить его с помощью BasePermissionController. Далее импортируем абстрактную функцию onPermissionResult.

class PermissionController : BasePermissionController() {
 
    override fun onPermissionResult(permissionResult: PermissionResult) {
        
    }
    
}

Теперь пришло время написать настоящую логику с помощью сопрограмм. Как только onPermissionResult будет вызван из основного контроллера, нам нужно передать permissionResult обратно на сайт вызова. Чтобы сделать это с помощью сопрограмм, мы используем CompletableDeferred:

Deferred  —  то, что может быть завершено с помощью публичных функций complete или cancel..

Все функции этого интерфейса [и все производные от него интерфейсы] потокобезопасны и могут быть безопасно вызваны из параллельных сопрограмм без внешней синхронизации”.  —  Kotlin на GitHub

Поэтому нужно создать экземпляр CompletableDeferred с типом PermissionResult и вызвать его в функции onPermissionResult:

class PermissionController : BasePermissionController() {
    
    private lateinit var completableDeferred: CompletableDeferred<PermissionResult>

    override fun onPermissionResult(permissionResult: PermissionResult) {
        if (::completableDeferred.isInitialized) {
            completableDeferred.complete(permissionResult)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (::completableDeferred.isInitialized && completableDeferred.isActive) {
            completableDeferred.cancel()
        }
    }
}

Быстрый доступ

Чтобы сделать обработку разрешений на месте вызова еще более плавной, можно создать общедоступную функцию на сопутствующем объекте PermissionController и написать шаблонный код:

/** вызов из Активности */
suspend fun requestPermissions(
    activity: AppCompatActivity,
    requestId: Int,
    vararg permissions: String
): PermissionResult {
    return withContext(Dispatchers.Main) {
        return@withContext _requestPermissions(
            activity,
            requestId,
            *permissions
        )
    }
}

/** Вызов из Фрагмента */
suspend fun requestPermissions(
    fragment: Fragment,
    requestId: Int,
    vararg permissions: String
): PermissionResult {
    return withContext(Dispatchers.Main) {
        return@withContext _requestPermissions(
            fragment,
            requestId,
            *permissions
        )
    }
}

Вызов 

На месте вызова  —  будь то действие или фрагмент  —  необходимо вызвать requestPermissions из функции suspend или области сопрограммы с Dispatcher.Main.

coroutineScope.launch {
    withContext(Dispatchers.Main) {
        val resultData = PermissionManager.requestPermissions(
                this@fragmentName, RESULT_CODE,
                Manifest.permission.CAMERA)
    }
}

Как только мы получаем данные, уже можно начинать их обработку с помощью ключевого слова when и перемещаться к состоянию. 

when (permissionResult) {
    is PermissionResult.PermissionGranted -> {
        // Все разрешения предоставлены
    }
    is PermissionResult.PermissionDenied -> {
        // Отказано в некоторых или во всех разрешениях
    }
    is PermissionResult.ShowRational -> {
        // Необходимо показать сообщение с причиной
    }
    is PermissionResult.PermissionDeniedPermanently -> {
        // В разрешениях отказано навсегда
    }
}

Ссылки и источники

На этом все. Надеюсь, вы узнали кое-что полезное. Спасибо за чтение!

Весь код, показанный в статье, взят с https://github.com/sagar-viradiya/eazypermissions

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Siva Ganesh Kantamani “When Coroutines Meet Android Permissions”

Предыдущая статья7 Must Visit ресурсов с идеями для веб-дизайна
Следующая статьяМашинное обучение без данных