Compose и другие библиотеки

В Compose можно использовать любимые библиотеки. В этом разделе рассказывается, как добавить некоторые из наиболее полезных библиотек.

Действия

Чтобы использовать Compose в действии, необходимо применить ComponentActivity – подкласс Activity, который предоставляет Compose подходящие LifecycleOwner и компоненты. Кроме того, в нем есть дополнительные API, которые позволяют отделить код от переопределяемых методов в классе действий. Activity Compose предоставляет эти API для композиций, поэтому вам больше не нужно переопределять методы вне композиций или получать явный экземпляр Activity. Кроме того, эти API гарантируют, что инициализация выполняется только один раз, что они сохраняются при повторной композиции и что они корректно очищаются, если компонуемый объект удаляется из композиции.

Результат действия

rememberLauncherForActivityResult() API позволяет получать результат из действия в вашей composable-функции:

@Composable
fun GetContentExample() {
    var imageUri by remember { mutableStateOf<Uri?>(null) }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        imageUri = uri
    }
    Column {
        Button(onClick = { launcher.launch("image/*") }) {
            Text(text = "Load Image")
        }
        Image(
            painter = rememberAsyncImagePainter(imageUri),
            contentDescription = "My Image"
        )
    }
}

В этом примере показан простой договор GetContent(). Нажав на кнопку, вы отправите запрос. Замыкание, следующее за rememberLauncherForActivityResult(), вызывается, когда пользователь выбирает изображение и возвращается к исходному действию. Выбранное изображение будет загружено с помощью функции rememberImagePainter() Coil.

Любой подкласс ActivityResultContract можно использовать в качестве первого аргумента для rememberLauncherForActivityResult(). Это означает, что вы можете использовать этот метод для запроса контента из фреймворка и в других распространенных случаях. Вы также можете создавать собственные пользовательские контракты и использовать их с этим методом.

Как запросить динамические разрешения

Тот же API результатов действий и rememberLauncherForActivityResult(), о котором мы говорили выше, можно использовать для запроса динамических разрешений с помощью контракта RequestPermission для одного разрешения или контракта RequestMultiplePermissions для нескольких разрешений.

Библиотеку разрешений Accompanist также можно использовать в качестве слоя над этими API, чтобы сопоставить текущий предоставленный статус разрешений со статусом, который может использовать ваш интерфейс Compose.

Обработка нажатия кнопки "Назад"

Чтобы настроить навигацию назад и переопределить поведение системной кнопки "Назад" в компоненте, используйте BackHandler для перехвата этого события:

var backHandlingEnabled by remember { mutableStateOf(true) }
BackHandler(backHandlingEnabled) {
    // Handle back press
}

Первый аргумент определяет, включен ли в данный момент параметр BackHandler. Вы можете использовать этот аргумент, чтобы временно отключить обработчик в зависимости от состояния компонента. Лямбда-функция будет вызвана, если пользователь активирует системное событие "Назад" и в данный момент включена функция BackHandler.

ViewModel

Если вы используете библиотеку Architecture Components ViewModel, то можете получить доступ к ViewModel из любой composable-функции, вызвав функцию viewModel(). Добавьте в файл Gradle следующую зависимость:

Яркий

dependencies {
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
}

Kotlin

dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0")
}

После этого вы сможете использовать функцию viewModel() в своем коде.

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

viewModel() возвращает существующий объект ViewModel или создает новый. По умолчанию возвращаемый объект ViewModel относится к текущему действию, фрагменту или пункту назначения навигации и сохраняется, пока существует область действия.

Например, если composable-функция используется в действии, viewModel() возвращает тот же экземпляр, пока действие не будет завершено или процесс не будет остановлен.

class MyViewModel : ViewModel() { /*...*/ }
// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    // Returns the same instance as long as the activity is alive,
    // just as if you grabbed the instance from an Activity or Fragment
    viewModel: MyViewModel = viewModel()
) { /* ... */ }

@Composable
fun MyScreen2(
    viewModel: MyViewModel = viewModel() // Same instance as in MyScreen
) { /* ... */ }

Рекомендации по использованию

Обычно доступ к экземплярам ViewModel осуществляется на уровне экрана, то есть рядом с корневым компонентом, вызываемым из действия, фрагмента или пункта назначения графа навигации. Это связано с тем, что по умолчанию ViewModels относятся к объектам на уровне экрана. Подробнее ViewModel о жизненном цикле и области действия…

Старайтесь не передавать экземпляры ViewModel другим композициям, так как это может затруднить тестирование и нарушить работу предварительного просмотра. Вместо этого передавайте только необходимые данные и функции в качестве параметров.

Вы можете использовать экземпляры ViewModel для управления состоянием композиций на уровне дочернего экрана, но учитывайте ViewModel жизненный цикл и область действия. Если компонент composable является автономным, вы можете использовать Hilt, чтобы внедрить ViewModel и избежать передачи зависимостей от родительских компонентов composable.

Если у вашего свойства ViewModel есть зависимости, viewModel() принимает необязательный параметр ViewModelProvider.Factory.

Подробнее о том, как использовать ViewModel в Compose и как экземпляры используются с библиотекой Navigation Compose, а также с действиями и фрагментами, можно узнать из документации по совместимости.

Потоки данных

Compose поставляется с расширениями для самых популярных решений на основе потоков в Android. Каждое из этих расширений предоставляется отдельным артефактом:

  • LiveData.observeAsState() включен в артефакт androidx.compose.runtime:runtime-livedata:$composeVersion.
  • Объект типа "Flow.collectAsState()" не требует дополнительных зависимостей.
  • Observable.subscribeAsState(), включенный в артефакт androidx.compose.runtime:runtime-rxjava2:$composeVersion или androidx.compose.runtime:runtime-rxjava3:$composeVersion.

Эти объекты регистрируются как прослушиватели и представляют значения в виде State. Когда появляется новое значение, Compose перекомпоновывает те части интерфейса, в которых используется state.value. Например, в этом коде ShowData перекомпонуется каждый раз, когда exampleLiveData выдает новое значение.

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val dataExample = viewModel.exampleLiveData.observeAsState()

    // Because the state is read here,
    // MyScreen recomposes whenever dataExample changes.
    dataExample.value?.let {
        ShowData(dataExample)
    }
}

Асинхронные операции в Compose

Jetpack Compose позволяет выполнять асинхронные операции с помощью сопрограмм из композиций.

Подробную информацию об API LaunchedEffect, produceState и rememberCoroutineScope можно найти в документации по побочным эффектам.

Компонент Navigation поддерживает приложения Jetpack Compose. Подробнее о том, как использовать навигацию с Compose и как перенести навигацию Jetpack в Compose…

Hilt

Hilt – это рекомендуемое решение для внедрения зависимостей в приложениях Android, которое без проблем работает с Compose.

Функция viewModel(), упомянутая в разделе о ViewModel, автоматически использует ViewModel, созданную Hilt с помощью аннотации @HiltViewModel. Мы подготовили документацию с информацией об интеграции Hilt с ViewModel.

@HiltViewModel
class MyViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repository: ExampleRepository
) : ViewModel() { /* ... */ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) { /* ... */ }

Hilt и навигация

Hilt также интегрируется с библиотекой Navigation Compose. Добавьте в файл Gradle следующие дополнительные зависимости:

Яркий

dependencies {
    implementation 'androidx.hilt:hilt-navigation-compose:1.4.0'
}

Kotlin

dependencies {
    implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
}

При использовании Navigation Compose всегда применяйте функцию hiltViewModel, чтобы получить экземпляр аннотированного @HiltViewModel ViewModel. Это работает с фрагментами или действиями, аннотированными с помощью @AndroidEntryPoint.

Например, если ExampleScreen – это пункт назначения в графе навигации, вызовите hiltViewModel(), чтобы получить экземпляр ExampleViewModel, ограниченный пунктом назначения, как показано в фрагменте кода ниже.

// import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun MyApp() {
    val navController = rememberNavController()
    val startRoute = "example"
    NavHost(navController, startDestination = startRoute) {
        composable("example") { backStackEntry ->
            // Creates a ViewModel from the current BackStackEntry
            // Available in the androidx.hilt:hilt-navigation-compose artifact
            val viewModel = hiltViewModel<MyViewModel>()
            MyScreen(viewModel)
        }
        /* ... */
    }
}

Если вам нужно получить экземпляр ViewModel, относящийся к маршрутам навигации или графу навигации, используйте composable-функцию hiltViewModel и передайте соответствующий параметр backStackEntry:

// import androidx.hilt.navigation.compose.hiltViewModel
// import androidx.navigation.compose.getBackStackEntry

@Composable
fun MyApp() {
    val navController = rememberNavController()
    val startRoute = "example"
    val innerStartRoute = "exampleWithRoute"
    NavHost(navController, startDestination = startRoute) {
        navigation(startDestination = innerStartRoute, route = "Parent") {
            // ...
            composable("exampleWithRoute") { backStackEntry ->
                val parentEntry = remember(backStackEntry) {
                    navController.getBackStackEntry("Parent")
                }
                val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)
                ExampleWithRouteScreen(parentViewModel)
            }
        }
    }
}

Страницы

Библиотека Paging позволяет загружать данные постепенно и поддерживается в Compose. На странице выпуска библиотеки Paging приведена информация о дополнительной зависимости paging-compose, которую нужно добавить в проект, и ее версии.

Вот пример API библиотеки Paging для Compose:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

Подробнее о том, как использовать разбиение на страницы в Compose, можно узнать из документации по спискам и сеткам.

Карты

Вы можете использовать библиотеку Maps Compose, чтобы добавить в приложение Google Карты. Вот пример использования:

@Composable
fun MapsExample() {
    val singapore = LatLng(1.35, 103.87)
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(singapore, 10f)
    }
    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState
    ) {
        Marker(
            state = remember { MarkerState(position = singapore) },
            title = "Singapore",
            snippet = "Marker in Singapore"
        )
    }
}