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

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

Действия

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

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

API rememberLauncherForActivityResult() позволяет получить результат действия в 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.

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

Запрос динамических разрешений

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

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

Как обрабатывать нажатие системной кнопки "Назад"

Чтобы настроить обратную навигацию и переопределить режим работы по умолчанию для системной кнопки "Назад" в composable-функции, используйте 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 осуществляется на уровне экрана, то есть рядом с корневой composable-функцией, вызываемой из Activity, фрагмента или пункта назначения графа Navigation. Это связано с тем, что ViewModel по умолчанию относятся к объектам на уровне экрана. Подробнее ViewModel о жизненном цикле и области действия…

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

Вы можете использовать экземпляры ViewModel для управления состоянием composable-функций на уровне подэкрана, но учитывайте жизненный цикл и область действия 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 позволяет выполнять асинхронные операции с помощью сопрограмм из composable-функций.

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

Компонент Navigation поддерживает приложения Jetpack Compose. Дополнительную информацию можно найти в статьях Навигация с помощью Compose и Перенос Jetpack Navigation в Navigation Compose.

Hilt

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

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

@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 и Navigation

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 всегда используйте composable-функцию hiltViewModel, чтобы получить экземпляр ViewModel с аннотацией @HiltViewModel. Это работает с фрагментами или действиями, помеченными как @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 Compose библиотеки Paging:

@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")
        }
    }
}

Дополнительную информацию об использовании Paging в Compose можно найти в документации по спискам и сеткам.

Карты

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

@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"
        )
    }
}