Найти тему
Nuances of programming

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

Оглавление

Источник: Nuances of Programming

Из этой статьи вы узнаете, как обрабатывать разрешения среды выполнения 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”