Процесс работы с пермишенами начинается с того что мы определяем класс Permissions который представляет собой Model и хранит в себе данные для отображения диалогов и сами пермишены:
// Сожержимое класса плохо структурировано и тут есть над чем подумать, к примеру rationale и explanation
// можно добавлять как объекты во внутреннюю коллекцию класса Permissions и тогда можно будет отображать разные
// типы сообщений для одних и тех же пермишенов. Также при таком варианте отпадет необходимость в создании промежуточных наследников DialogPermissions.
object LocationPermissions : DialogPermissions { // диалог так как хотим показывать их в виде диалога
override val rationaleTitle = R.string.rationaleTitle
override val rationaleMessage = R.string.rationaleMessage
override val explanationTitle = null // не указываем так как не хотим показывать диалог разъяснения
override val explanationMessage = null
override val list = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
}Пермишены могут храниться в :shared:permission модуле или быть объявлены непосредственно в фиче, если для нее нужен какой то специфичный текст или вдруг в обосновании мы хотим показывать картинки.
Таким образом разные экраны могут поразному отображать визуально обоснования (в виде диалога / тоста / view на экране), а также для одних и тех же пермишенов может быть разный текст обоснования. К тому же мы можем в разных ситуациях гибко управлять тем, стоит ли нам показать обоснование или же сразу запросить разрешения. Взглянув на гайдлайны можно понять, что данный функционал может быть востребован, так как хорошие запросы пермишинов обычно тесно связанны с контекстом происходящего на экране, а соответственно не всегда нужно реагировать диалогами.
Далее взгляним на фиче рагмент:
class SomeFeatureFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
component.permissionsManager.attach(this, lifecycle, dialogQuestionHandler())
}
}Чтобы не фильтровать ответы от permissionsManager между различными экранами приложения, есть предложение не прокидывать менежджер через зависимости, а создавать его внутри экранного компонента фичи, таким образом у каждого экрана свой permissionsManager.
Далее менеджер аттачится к жизненному циклу и если необходимо показывать диалоги обоснования то указывается QuestionHandler. Что он из себя представляет:
// Апи сыровато, может можно как то подргому пробросить в permissionsManager shouldShowRequestPermissionRationale, а также как то более удачно обработать Continuation.
interface QuestionHandler {
fun shouldShowRequestPermissionRationale(permission: String): Boolean
// показать обоснование перед запросом разрешений
fun showRationale(permissions: Permissions, continuation: Continuation)
// показать объяснение что функции не доступны так как в разрешении отказано (показывается когда пользователь отказывает в разрешениях либо на диалоге обоснования либо при запросе)
fun showExplanation(permissions: Permissions )
// показать запрос на переход в настройки и предоставления разрешения (вызывается когда при запросе происходит сразу же отказ)
fun showSettings(permissions: Permissions, continuation: Continuation)
}Реализация QuestionHandler может находиться в :shared:permissions: и выглядеть следующим образом:
fun Fragment.dialogQuestionHandler(): QuestionHandler {
// в реале берем фрагмент менеджер и достаем / создаем PermissionsDialogFragment
return PermissionsDialogFragment()
}
class PermissionsDialogFragment : Fragment(), QuestionHandler {
override fun showRationale(permissions: Permissions, continuation: Continuation) {
// кастим Permissions к DialogPermissions если не скастились значить данный пермишен мы не обрабатывам и диалог не показываем.
// если скастились то показываем диалог после чего в его калбеках на кнопках вызываем
continuation.next() // идем дальше по флоу
continuation.cancel() // отменяем запрос
// Тут есть проблема с тем чтобы красиво пробросить в GoofyDialog continuation, пока самое простое хранить последний continuation в поле и в колбеке onDialogActionEvent забирать его и занулять поле.
}
....
}Тут представленна стандартная реализация, когда необходимо показывать диалоги. Но нам ничего не мешает вместо нее указать напрямую SomeFeatureFragment и реализовать на нем QuestionHandler, чтобы реализовать поведение специфичное для экрана и отображать кие то свои диалоги. Также мы можем комбинировать QuestionHandler, нужно всего лишь если мы не хотим обрабатывать какой то конкретный тип пермишенов делегировать его другому QuestionHandler:
class SomeFeatureFragment : Fragment(), QuestionHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
component.permissionsManager.attach(this, lifecycle, this)
}
override fun showRationale(permissions: Permissions, continuation: Continuation) {
when(permissions) {
is DialogPermissions -> dialogQuestionHandler().showRationale(permissions, continuation)
else -> // show something ui item
}
}
}В принципе на этом работа с пермишенами во View заканчивается. Если подытожить то мы формируем :shared:permission который хранит реализации QuestionHandler и Permissions общих для приложения и завязанных на наших ресурсах / дизайне / архитектуре. Также у нас появляется библиотечный модуль :library:permission в котором храниться сам PermissionsManager и необходимые для его работы классы. Далее фича используя эти два модуля вызывает во фрагменте одну строчку (если не нужно кастомизировать отображение сообщений обоснования):
component.permissionsManager.attach(this, lifecycle, dialogQuestionHandler())Теперь посмотрим на api со стороны ViewModel:
class SomeViewModel @Inject constructor(
private val permissionsManager: PermissionsManager
) : ViewModel() {
init {
permissionsManager.observe(LocationPermissions)
.collect {
when (it) {
Granted -> // запускаем запрошенный функционал
Denied -> // деградируем ui чтобы работать без разрешений для этого отправляем новый стейт во View
Cancelled -> // к этому калбеку у меня вопросы, когда такое реально может потребоваться? ведь это синоним Denied.
}
}.launchIn(viewModelScope)
}
internal fun handleAction(action: TrapMainViewAction) {
when (action) {
is ShowLocation -> permissionsManager.request(LocationPermissions) // как правило запуск любой функции закрытой пермишенами осуществояется по средством запроса, вся логика по работе функуии размещается в Granted.
}
}
}Также у permissionsManager есть метод fun isGranted(permissions: Permissions): Boolean чтобы синхронно получить текущее состояние пермишенов, это может быть полезно на этапе загрузки экрана чтобы отрисовать начальный стейт.
Пока для ViewModel в PermissionsManager реализован описанный выше вариант. Однако есть мысли представить все немного иначе с точки зрения api, примерно как то так:
class SomeViewModel @Inject constructor(
private val permissionsManager: PermissionsManager
) : ViewModel() {
val showLocation = permissionsManager.protectedFunction(LocationPermissions)
.granted { /* показываем местоположение */ }
.denied { /* что то делаем с ui */ }
.launchIn(viewModelScope)
internal fun handleAction(action: TrapMainViewAction) {
when (action) {
is ShowLocation -> showLocation.invoke()
}
}
}Идея сырая и над api / неймингом стоит поработать, но основной посыл такой что, мы создаем защищенные функции вызов которых приводит к проверке и запросу разрешений если это требуется. После того как разрешения предоставлены функция выполняется. Также у защищенной функции есть возможность проверить разрешена она или нет showLocation.isGranted().
Внутренности модуля :library:permission можно глянуть в этом gist. Решение не закончено, нужно реализовать часть методов, обдумать нейминг и api. Но основная идея в том чтобы добавить гибкости кастомизации и сделать решение независимым от нашего проекта.