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



Книга Корутины и управление разрешениями в 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) {
[email protected] _requestPermissions(
activity,
requestId,
*permissions
)
}
}

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

Вызов 


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


coroutineScope.launch {
withContext(Dispatchers.Main) {
val resultData = PermissionManager.requestPermissions(
[email protected], 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


883   0  

Comments

    Ничего не найдено.