Корутины и управление разрешениями в Android.
Из этой статьи вы узнаете, как обрабатывать разрешения среды выполнения Android, появившиеся в Android Marshmallow, с помощью корутин (сопрограмм). Такой подход позволит обрабатывать разрешения в компонентах Android с минимальным количеством кода. Вам больше не нужно будет иметь дело с функциями обратного вызова и onResult. Обзор
Разберем пример с достаточно несложным процессом. Нам нужно создать базовый фрагмент для обработки всех необходимых разреш
Корутины и управление разрешениями в 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.