Compose y otras bibliotecas

Puedes usar tus bibliotecas favoritas en Compose. En esta sección se describe cómo incorporar algunas de las bibliotecas más útiles.

Activity

Para usar Compose en una actividad, debes usar ComponentActivity, una subclase de Activity que proporciona LifecycleOwner y los componentes correctos para Compose. También brinda las API adicionales que separan el código de los métodos de anulación en la clase de actividad. Activity Compose expone estas API a elementos que admiten composición, de modo que ya no es necesario anular métodos fuera de estos elementos ni recuperar una instancia Activity explícita. Además, estas API garantizan que solo se inicialicen una vez, permanezcan vigentes tras la recomposición y se borren, de manera correcta, si se quita de la composición el elemento que la admite.

ActivityResult

La API de rememberLauncherForActivityResult() te permite obtener un resultado de una actividad en el elemento que admite composición:

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

En este ejemplo, se muestra un contrato GetContent() simple. Cuando se presiona el botón, se inicia la solicitud. Se invoca la lambda final para rememberLauncherForActivityResult() una vez que el usuario selecciona una imagen y regresa a la actividad de inicio. De esta manera, se carga la imagen seleccionada con la función rememberImagePainter() de Coil.

Cualquier subclase de ActivityResultContract puede usarse como primer argumento para rememberLauncherForActivityResult(). Es decir, puedes usar esta técnica para solicitar contenido desde el framework y en otros patrones comunes. También puedes crear tus propios contratos personalizados y usarlos con esta técnica.

Cómo solicitar permisos de tiempo de ejecución

La misma API de Activity Result y rememberLauncherForActivityResult() que se explicaron anteriormente se pueden usar para solicitar permisos de tiempo de ejecución con el contrato RequestPermission para un único permiso o RequestMultiplePermissions para varios permisos.

La biblioteca de permisos de acompañamiento también se puede usar en una capa encima de esas APIs para asignar el estado actual que se otorga a los permisos en un estado que puede usar la IU de Compose.

Cómo controlar el botón Atrás del sistema

Para brindar navegación personalizada hacia atrás y anular el comportamiento predeterminado del botón Atrás del sistema desde el elemento que admite composición, este elemento puede usar BackHandler a fin de interceptar ese evento:

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

El primer argumento controla si, en ese momento, el objeto BackHandler está habilitado; puedes usar este argumento para inhabilitar el controlador de forma temporal, según el estado del componente. Se invocará la lambda final si el usuario activa un evento del botón Atrás del sistema y si, en ese momento, BackHandler está habilitado.

ViewModel

Si usas la biblioteca de ViewModel de componentes de arquitectura, puedes llamar a la función viewModel() para acceder a un objeto ViewModel desde cualquier elemento que admite composición. Agrega la siguiente dependencia a tu archivo de Gradle:

Groovy

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

Kotlin

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

Luego, puedes usar la función viewModel() en tu código.

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

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

viewModel() muestra un ViewModel existente o crea uno nuevo. De forma predeterminada, el ViewModel que se muestra se define en el alcance de la actividad, el fragmento o el destino de navegación que lo contiene, y se conserva mientras el alcance esté activo.

Por ejemplo, si el elemento componible se usa en una actividad, viewModel() muestra la misma instancia hasta que finaliza la actividad o se cierra el proceso.

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
) { /* ... */ }

Lineamientos de uso

Por lo general, accedes a instancias de ViewModel en elementos componibles al nivel de la pantalla, es decir, cerca de un elemento componible raíz que se llama desde una actividad, un fragmento o un destino de un gráfico de Navigation. Esto se debe a que, de forma predeterminada, los ViewModel se asignan a esos objetos a nivel de la pantalla. Obtén más información sobre el ciclo de vida y el alcance de un ViewModel aquí.

Intenta evitar pasar instancias de ViewModel a otros elementos componibles, ya que esto puede dificultar su prueba y dañar las vistas previas. En su lugar, pasa solo los datos y las funciones que necesitan como parámetros.

Puedes usar instancias de ViewModel para administrar el estado de los elementos componibles a nivel de la subpantalla. Sin embargo, ten en cuenta el ciclo de vida y el alcance de ViewModel. Si el elemento componible es independiente, te recomendamos que uses Hilt para insertar el ViewModel y evitar tener que pasar dependencias de elementos componibles superiores.

Si tu ViewModel tiene dependencias, viewModel() toma un objeto ViewModelProvider.Factory opcional como parámetro.

Para obtener más información sobre ViewModel en Compose y el modo en que se usan las instancias con la biblioteca de navegación de Compose, o sobre actividades y fragmentos, consulta los documentos de interoperabilidad.

Flujos de datos

Compose incluye extensiones para las soluciones basadas en transmisión más populares de Android. Cada una de estas extensiones es proporcionada por otro artefacto:

Estos artefactos se registran como objetos de escucha y representan los valores como un elemento State. Cada vez que se emite un valor nuevo, Compose recompone esas partes de la IU en las que se usa ese state.value. Por ejemplo, en este código, se recompone ShowData cada vez que exampleLiveData emite un valor nuevo.

// 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)
    }
}

Operaciones asíncronas en Compose

Jetpack Compose te permite usar corrutinas desde tus elementos componibles a fin de ejecutar operaciones asíncronas.

Consulta las API de LaunchedEffect, produceState y rememberCoroutineScope en la documentación sobre efectos secundarios para obtener más información.

El componente Navigation proporciona compatibilidad con aplicaciones de Jetpack Compose. Consulta Cómo navegar con Compose y Cómo migrar Jetpack Navigation a Navigation Compose para obtener más información.

Hilt

Hilt es la solución recomendada para la inyección de dependencias en las apps para Android y funciona a la perfección con Compose.

La función viewModel() mencionada en la sección ViewModel usa automáticamente el ViewModel que crea Hilt con la anotación @HiltViewModel. Incluimos documentación sobre la integración de ViewModel de 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 y Navigation

Hilt también se integra con la biblioteca de navegación de Compose. Agrega las siguientes dependencias adicionales a tu archivo de Gradle:

Groovy

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

Kotlin

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

Cuando uses la navegación de Compose, usa siempre la función de componibilidad hiltViewModel para obtener una instancia de ViewModel con anotaciones @HiltViewModel. Esto funciona con fragmentos o actividades con anotaciones @AndroidEntryPoint.

Por ejemplo, si ExampleScreen es un destino en un gráfico de navegación, llama a hiltViewModel() para obtener una instancia de ExampleViewModel con alcance al destino, como se muestra en el fragmento de código a continuación:

// 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)
        }
        /* ... */
    }
}

Si necesitas recuperar la instancia de un ViewModel con alcance a rutas de navegación o el gráfico de navegación, usa la función de componibilidad hiltViewModel y pasa el backStackEntry correspondiente como parámetro:

// 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

La biblioteca de Paging facilita la carga gradual de datos y es compatible con Compose. La página de versiones de Paging contiene información sobre la dependencia adicional paging-compose que se debe agregar al proyecto y a su versión.

A continuación, se muestra un ejemplo de las API de Compose de la biblioteca de 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")
        }
    }
}

Consulta la documentación de listas y cuadrículas para obtener más información sobre el uso de Paging en Compose.

Maps

Puedes usar la biblioteca de Maps Compose para ofrecer Google Maps en tu app. A continuación, se muestra un ejemplo de uso:

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