Avatar ·

Как проверить и запросить разрешения (permissions) во фрагменте (Fragment) Android на Kotlin?

Всем привет! Я новичок в Android-разработке на Kotlin, и у меня возникла проблема с запросом разрешений (permissions) внутри фрагмента. Я читал много примеров, но все они в основном показывают, как делать это из Activity, используя `onRequestPermissionsResult`. Когда я пытаюсь вызвать `requestPermissions()` из фрагмента, ничего не происходит, или результат приходит в Activity, а не во фрагмент. Вот мой упрощённый код. У меня есть фрагмент, который должен получить доступ к камере. ```kotlin class CameraFragment : Fragment() { private lateinit var binding: FragmentCameraBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentCameraBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.buttonOpenCamera.setOnClickListener { // Пытаюсь запросить разрешение на камеру if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { openCamera() } else { // Запрашиваю разрешение requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_CODE) } } } @Deprecated("Deprecated in Java") override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == CAMERA_PERMISSION_CODE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { openCamera() } else { Toast.makeText(requireContext(), "Разрешение не получено", Toast.LENGTH_SHORT).show() } } } private fun openCamera() { // Логика открытия камеры } companion object { private const val CAMERA_PERMISSION_CODE = 100 } } ``` Проблема в том, что `onRequestPermissionsResult` во фрагменте иногда не вызывается, или я получаю странные ошибки. Я слышал, что старый метод `onRequestPermissionsResult` устарел и нужно использовать что-то другое. Как правильно запрашивать разрешения во фрагменте на Kotlin, чтобы это работало надёжно? Нужно ли как-то передавать результат из Activity во фрагмент? Буду благодарен за подробное объяснение! <<>>

Проблема и правильное решение

Вы столкнулись с классической проблемой Android-разработки. Старый метод onRequestPermissionsResult действительно устарел и может работать нестабильно во фрагментах, особенно если вы используете библиотеки, которые перехватывают результат (например, Navigation Component или ViewPager). Правильный и современный подход — использовать Activity Result API, который был добавлен в AndroidX Activity 1.2.0 и Fragment 1.3.0.

Activity Result API предоставляет типобезопасный способ запроса разрешений и получения результатов. Вам больше не нужно переопределять onRequestPermissionsResult — вы просто регистрируете "контракт" на запрос разрешения и получаете результат через лямбду или колбэк.

Пошаговая реализация

Вот как правильно запросить разрешение камеры во фрагменте с использованием Activity Result API:

class CameraFragment : Fragment() {

    private lateinit var binding: FragmentCameraBinding

    // 1. Создаём контракт для запроса разрешения
    private val requestCameraPermission = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Разрешение получено — открываем камеру
            openCamera()
        } else {
            // Разрешение не получено — показываем сообщение
            Toast.makeText(requireContext(), "Разрешение на камеру не получено", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentCameraBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.buttonOpenCamera.setOnClickListener {
            // 2. Проверяем, есть ли уже разрешение
            if (ContextCompat.checkSelfPermission(
                    requireContext(),
                    Manifest.permission.CAMERA
                ) == PackageManager.PERMISSION_GRANTED
            ) {
                openCamera()
            } else {
                // 3. Если разрешения нет — запрашиваем его
                requestCameraPermission.launch(Manifest.permission.CAMERA)
            }
        }
    }

    private fun openCamera() {
        // Логика открытия камеры
        Toast.makeText(requireContext(), "Камера открыта", Toast.LENGTH_SHORT).show()
    }

    // ВАЖНО: метод onRequestPermissionsResult больше НЕ НУЖЕН!
}

Почему это работает и в чём преимущества

  • Типобезопасность: Вы передаёте конкретное разрешение (String) и получаете Boolean. Нет риска перепутать requestCode.
  • Локализация во фрагменте: Результат приходит непосредственно во фрагмент, а не в Activity. Вам не нужно писать код в Activity для передачи результата.
  • Совместимость с жизненным циклом: registerForActivityResult автоматически отменяет регистрацию, когда фрагмент уничтожается. Это предотвращает утечки памяти.
  • Нет необходимости в override: Код становится чище и короче.

Дополнительно: объяснение пользователю (Rationale)

Иногда пользователь может один раз отклонить разрешение. В этом случае стоит показать диалог с объяснением, зачем нужно разрешение. Activity Result API позволяет легко обработать этот сценарий:

private val requestCameraPermission = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted ->
    if (isGranted) {
        openCamera()
    } else {
        // Проверяем, нужно ли показать объяснение
        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
            // Показываем диалог с объяснением
            AlertDialog.Builder(requireContext())
                .setTitle("Разрешение на камеру")
                .setMessage("Для работы приложения необходим доступ к камере. Пожалуйста, разрешите его в настройках.")
                .setPositiveButton("OK") { _, _ ->
                    requestCameraPermission.launch(Manifest.permission.CAMERA)
                }
                .setNegativeButton("Отмена", null)
                .show()
        } else {
            Toast.makeText(requireContext(), "Разрешение отклонено навсегда", Toast.LENGTH_SHORT).show()
        }
    }
)

Важные замечания

  • Не забудьте добавить разрешение в AndroidManifest.xml: <uses-permission android:name="android.permission.CAMERA" />
  • Если вы используете несколько разрешений одновременно, используйте ActivityResultContracts.RequestMultiplePermissions() — он возвращает Map<String, Boolean>.
  • Для API < 23 (Android 6.0) разрешения выдаются автоматически при установке, поэтому проверка не нужна. Activity Result API сам обрабатывает это.

Теперь ваш код будет работать надёжно во всех сценариях, и вам не придётся беспокоиться о передаче результатов между Activity и Fragment.

<<>>

Альтернативный подход: использование делегата из библиотеки KTX

Если вы используете Kotlin и хотите ещё более лаконичный код, можно воспользоваться библиотекой AndroidX Activity KTX и Kotlin-делегатами. Однако учтите, что этот способ требует дополнительной зависимости в build.gradle:

implementation "androidx.activity:activity-ktx:1.8.0"

Вот пример использования с делегатом rememberLauncherForActivityResult (этот подход больше подходит для Jetpack Compose, но может быть адаптирован и для View-фрагментов):

class CameraFragment : Fragment() {

    private val cameraPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        if (isGranted) {
            openCamera()
        }
    }

    // ... остальной код
}

На самом деле, это тот же Activity Result API, просто оформленный через функцию-расширение. Разница минимальна, но если вы привыкли к функциональному стилю, можете использовать ActivityResultLauncher напрямую без registerForActivityResult в некоторых случаях, передавая его как параметр.

Когда это может быть полезно?

  • Если у вас много фрагментов с одинаковыми запросами разрешений — вы можете вынести логику в базовый класс или утилитарную функцию.
  • Если вы используете архитектуру MVVM и хотите запрашивать разрешения из ViewModel — в этом случае Activity Result API не подходит (ViewModel не имеет доступа к контексту Activity), и нужно использовать другие подходы (например, Event-ы).

В любом случае, основной рекомендуемый способ для фрагментов — это