Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Interoperabilidad de Compose

Jetpack Compose está diseñado para funcionar con el enfoque establecido de IU basada en vistas. Si estás compilando una app nueva, la mejor opción podría ser implementar la IU completa con Compose. Sin embargo, si estás modificando una app existente, es posible que no quieras migrarla. En ese caso, puedes combinar Compose con tu diseño de IU existente.

Existen dos formas principales de combinar Compose con una IU basada en vistas:

  • Para agregar elementos de Compose a tu IU existente, puedes crear una pantalla completamente nueva basada en Compose o hacerlo sobre un diseño de vistas o fragmento existente.
  • Puedes agregar un elemento de la IU basada en vistas a tus funciones que admiten composición. Esto te permitirá agregar widgets que no sean de Compose en un diseño basado en Compose.

Compose en vistas de Android

Puedes agregar una IU basada en Compose a una app existente que use un diseño basado en vistas.

Para crear una pantalla nueva basada íntegramente en Compose, haz que tu actividad llame al método setContent() y pasa las funciones que admiten composición que quieras.

class ExampleActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent { // In here, we can call composables!
      MaterialTheme {
        Greeting(name = "compose")
      }
    }
  }
}

@Composable
fun Greeting(name: String) {
      Text (text = "Hello $name!")
}

Este código es similar al que se encuentra en una app exclusivamente de Compose.

Si quieres incorporar contenido de la IU de Compose a un fragmento o un diseño de vistas existente, usa ComposeView y llama a su método setContent(). ComposeView es una View de Android. Debes acoplar el ComposeView a un ViewTreeLifecycleOwner. El ViewTreeLifecycleOwner permite que la vista se acople y se desacople de forma repetitiva mientras se conserva la composición. ComponentActivity, FragmentActivity y AppCompatActivity son ejemplos de clases que implementan ViewTreeLifecycleOwner.

Puedes colocar la ComposeView en tu diseño XML como cualquier otra View:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

    <ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

En el código fuente de Kotlin, aumenta el diseño del recurso de diseño que se define en XML. Luego, obtén la ComposeView con el ID de XML y llama a setContent() para usar Compose.

class ExampleFragment : Fragment() {

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    // Inflate the layout for this fragment
    return inflater.inflate(
      R.layout.fragment_example, container, false
    ).apply {
      findViewById<ComposeView>(R.id.compose_view).setContent {
        // In Compose world
        MaterialTheme {
          Text("Hello Compose!")
        }
      }
    }
  }
}

Dos elementos de texto ligeramente diferentes, uno sobre el otro

Figura 1: Muestra el resultado del código que agrega elementos de Compose a una jerarquía de la IU de vistas. El texto "Hello Android!" se muestra en un widget TextView. El texto "Hello Compose!" se muestra en un elemento de texto de Compose.

También puedes incluir una ComposeView directamente en un fragmento si toda tu pantalla está compilada con Compose, lo que te permite no tener que usar un archivo de diseño XML.

class ExampleFragment : Fragment() {

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    return ComposeView(requireContext()).apply {
      setContent {
        MaterialTheme {
          // In Compose world
          Text("Hello Compose!")
        }
      }
    }
  }
}

Si hay varios elementos ComposeView en el mismo diseño, cada uno debe tener un ID único para que savedInstanceState funcione. Encontrarás más información al respecto en la sección SavedInstanceState.

class ExampleFragment : Fragment() {

  override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
        id = R.id.compose_view_x
        ...
      }
      addView(TextView(...))
      addView(ComposeView(...).apply {
        id = R.id.compose_view_y
        ...
      }
    }
  }
}

Los ID de ComposeView se definen en el archivo res/values/ids.xml:

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

Vistas de Android en Compose

Puedes incluir una jerarquía de vistas de Android en una IU de Compose. Este enfoque es particularmente útil si quieres usar elementos de la IU que aún no están disponibles en Compose, como AdView o MapView. Además, te permite volver a usar vistas personalizadas que ya hayas diseñado.

Para incluir un elemento o jerarquía de vistas, usa el elemento AndroidView. AndroidView recibe una expresión lambda que muestra una View. AndroidView también proporciona una devolución de llamada update que se realiza cuando se aumenta la vista. La AndroidView se recompone cada vez que cambia una lectura de State dentro de la devolución de llamada.

@Composable
fun CustomView() {
  val selectedItem = remember { mutableStateOf(0) }

  val context = ContextAmbient.current
  val customView = remember {
    // Creates custom view
    CustomView(context).apply {
      // Sets up listeners for View -> Compose communication
      myView.setOnClickListener {
        selectedItem.value = 1
      }
    }
  }

  // Adds view to Compose
  AndroidView({ customView }) { view ->
    // View's been inflated - add logic here if necessary

    // As selectedItem is read here, AndroidView will recompose
    // whenever the state changes
    // Example of Compose -> View communication
    view.coordinator.selectedItem = selectedItem
  }
}

@Composable
fun ContentExample() {
  Column(Modifier.fillMaxSize()) {
    Text("Look at this CustomView!")
    CustomView()
  }
}

Para incorporar un diseño XML, usa la API de AndroidViewBinding, que proporciona la biblioteca androidx.compose.ui:ui-viewbinding. Para ello, tu proyecto debe habilitar la vinculación de vistas.

@Composable
fun AndroidViewBindingExample() {
  AndroidViewBinding(ExampleLayoutBinding::inflate) {
    exampleView.setBackgroundColor(Color.GRAY)
  }
}

Cómo llamar al sistema de vistas desde Compose

El framework de Compose ofrece varias API que permiten que tu código de Compose interactúe con una IU basada en vistas.

Recursos del sistema

El framework de Compose ofrece métodos auxiliares ...Resource() que permiten que tu código de Compose obtenga recursos de una jerarquía de la IU basada en vistas. Estos son algunos ejemplos:

Text(
  text = stringResource(R.string.ok),
  modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)

Icon(
  assert = vectorResource(R.drawable.ic_plane),
  tint = colorResource(R.color.Blue700)
)

Contexto

La propiedad ContextAmbient.current te proporciona el contexto actual. Por ejemplo, este código crea una vista en el contexto actual:

@Composable
fun rememberCustomView(): CustomView {
  val context = ContextAmbient.current
  return remember { CustomView(context).apply { ... } }
}

Otras interacciones

Si no existe una utilidad definida para la interacción que necesitas, te recomendamos que sigas el lineamiento general de Compose, que los datos circulen hacia abajo y los eventos hacia arriba (que se aborda en más detalle. en Acerca de Compose). Por ejemplo, este elemento que admite composición ejecuta otra actividad:

class ExampleActivity : AppCompatActivity {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // get data from savedInstanceState
    setContent {
      MaterialTheme {
        ExampleComposable(data, onButtonClick = {
          startActivity(...)
        })
      }
    }
  }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
  Button(onClick = onButtonClick) {
    Text(data.title)
  }
}

Integración con bibliotecas comunes

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

ViewModel

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

class ExampleViewModel() : ViewModel() { ... }

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()

  ... // use viewModel here
}

viewModel() muestra una ViewModel existente o crea una nueva dentro del alcance dado. Se conservará ViewModel mientras esté activo el alcance. Por ejemplo, si el elemento se usa en una actividad, viewModel() muestra la misma instancia hasta que finaliza la actividad o se cierra el proceso.

@Composable
fun MyExample() {
  // Returns the same instance as long as the activity is alive,
  // just as if you grabbed the instance from an Activity or Fragment
  val viewModel: ExampleViewModel = viewModel()
}

@Composable
fun MyExample2() {
  val viewModel: ExampleViewModel = viewModel() // Same instance as in MyExample
}

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

Flujos de datos

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

Estos artefactos se registran como objetos de escucha y representan los valores como un 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.

@Composable
fun MyExample() {
  val viewModel: ExampleViewModel = viewModel()
  val dataExample = viewModel.exampleLiveData.observeAsState()

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

Operaciones asíncronas en Compose

Compose proporciona mecanismos que te permiten ejecutar operaciones asíncronas desde dentro de tus elementos que admiten composición.

Para las API basadas en devoluciones de llamadas, puedes usar una combinación de MutableState y onCommit(). Usa MutableState para almacenar el resultado de una devolución de llamada y recomponer la IU afectada cuando el resultado cambia. Usa onCommit() para ejecutar una operación cada vez que cambie un parámetro. También puedes definir un método onDispose() para borrar las operaciones pendientes si la composición se completa antes de que finalice la operación. En el siguiente ejemplo se muestra cómo estas API trabajan juntas.

@Composable
fun fetchImage(url: String): ImageAsset? {
    // Holds our current image, and will be updated by the onCommit lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    onCommit(url) {
        // This onCommit lambda will be invoked every time url changes

        val listener = object : ExampleImageLoader.Listener() {
            override fun onSuccess(bitmap: Bitmap) {
                // When the image successfully loads, update our image state
                image = bitmap.asImageAsset()
            }
        }

        // Now execute the image loader
        val imageLoader = ExampleImageLoader.get()
        imageLoader.load(url).into(listener)

        onDispose {
            // If we leave composition, cancel any pending requests
            imageLoader.cancel(listener)
        }
    }

    // Return the state-backed image property. Any callers of this function
    // will be recomposed once the image finishes loading
    return image
}

Si la operación asíncrona es una función de suspensión, puedes usar launchInComposition() en su lugar:

/** Example suspending loadImage function */
suspend fun loadImage(url: String): Bitmap

@Composable
fun fetchImage(url: String): ImageAsset? {
    // This holds our current image, and will be updated by the
    // launchInComposition lambda below
    var image by remember(url) { mutableStateOf<ImageAsset?>(null) }

    // launchInComposition will automatically launch a coroutine to execute
    // the given block. If the `url` changes, any previously launched coroutine
    // will be cancelled, and a new coroutine launched.
    launchInComposition(url) {
        image = loadImage(url)
    }

    // Return the state-backed image property
    return image
}

SavedInstanceState

Usa savedInstanceState para restablecer el estado de tu IU después de volver a crear una actividad o un proceso. savedInstanceState conserva el estado en todas las recomposiciones. Además, savedInstanceState también lo conserva en toda la recreación de la actividad y el proceso.

@Composable
fun MyExample() {
  var selectedId by savedInstanceState<String?> { null }
  ...
}

Todos los tipos de datos que se agregan a Bundle se guardan automáticamente. Si deseas guardar algo que no se puede agregar a Bundle, tienes varias opciones.

La solución más simple es agregar la anotación @Parcelize al objeto. El objeto se vuelve parcelable y se puede empaquetar. Por ejemplo, este código hace que un tipo de datos City se vuelva parcelable y lo guarda en el estado.

@Parcelize
data class City(name: String, country: String): Parcelable

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}

Si la alternativa @Parcelize no es adecuada por algún motivo, puedes usar mapSaver para definir tu propia regla de conversión de objetos en conjuntos de valores que el sistema pueda guardar en Bundle.

data class City(name: String, country: String)

val CitySaver = run {
  val nameKey = "Name"
  val countryKey = "Country"
  mapSaver(
    save = { mapOf(nameKey to it.name, nameKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
  )
}

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
}

Para evitar tener que definir las leyendas del mapa, también puedes usar listSaver y emplear sus índices como leyendas:

data class City(name: String, country: String)

val CitySaver = listSaver<City, Any>(
  save = { listOf(it.name, it.country) },
  restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
  ...
}

Temas

Si usas Material Design Components para Android en tu app, la biblioteca Adaptador de temas de MDC te permite volver a usar fácilmente los parámetros de color, tipografía y forma de tus temas existentes, desde dentro de tus elementos que admiten composición:

class ExampleActivity : AppCompatActivity {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
      // We use MdcTheme instead of MaterialTheme {}
      MdcTheme {
        ExampleComposable(...)
      }
    }
  }
}

Prueba

Puedes probar tu código combinado de vistas y Compose con la API createAndroidComposeRule(). Para obtener más información, consulta Cómo probar tu diseño de Compose.

Más información

Para obtener más información sobre cómo integrar Jetpack Compose a tu IU existente, prueba el codelab Cómo migrar a Jetpack Compose.