Лучший опыт

Корутины и управление разрешениями в 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