Из этой статьи вы узнаете, как обрабатывать разрешения среды выполнения 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.
Читайте также:
- Однонаправленный поток данных в пользовательском интерфейсе Android
- Как удаленно отлаживать сайты для Android с помощью Chrome DevTools
- Советы по модуляризации приложений Android
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи: Siva Ganesh Kantamani “When Coroutines Meet Android Permissions”