Cuando implementas Compose en tu app, puedes combinar IU basadas en View y de Compose. A continuación, se muestra una lista de API, recomendaciones y sugerencias para facilitar la transición a Compose.
Compose en View
Puedes agregar una IU basada en Compose a una app existente que use un diseño basado en View.
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 : ComponentActivity() {
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.
ViewCompositionStrategy para ComposeView
De forma predeterminada, Compose elimina la composición cuando la vista se separa de una ventana. Los tipos de View
de la IU de Compose, como ComposeView
y AbstractComposeView
, usan ViewCompositionStrategy
para definir este comportamiento.
De forma predeterminada, Compose usa la estrategia DisposeOnDetachedFromWindow
. Sin embargo, este valor predeterminado podría no ser deseable cuando se usan los tipos de View
de la IU de Compose en los siguientes elementos:
Fragmentos: La composición debe seguir el ciclo de vida de la vista del fragmento para los tipos de
View
de la IU de Compose a fin de guardar el estado.Transiciones: Cada vez que se use la
View
de la IU de Compose como parte de una transición, se separará de la ventana cuando comience y no cuando finalice, por lo que eliminará su estado mientras aún esté en pantalla.Contenedores de vista
RecyclerView
o tu propiaView
administrada durante el ciclo de vida.
En algunas de estas situaciones, es posible que la app también pierda memoria lentamente a partir de instancias de composición, a menos que llames de forma manual a AbstractComposeView.disposeComposition
.
Para eliminar automáticamente composiciones cuando ya no sean necesarias, configura una estrategia diferente o crea una con llamadas al método setViewCompositionStrategy
. Por ejemplo, la estrategia DisposeOnLifecycleDestroyed
elimina la composición cuando se destruye lifecycle
. Esta estrategia es adecuada para los tipos de View
de la IU de Compose que se relacionan de manera equivalente con un LifecycleOwner
conocido.
Cuando no se conoce el elemento LifecycleOwner
, se puede usar DisposeOnViewTreeLifecycleDestroyed
.
Observa cómo funciona esta API en la sección sobre ComposeView en fragmentos.
ComposeView en fragmentos
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.
Puedes colocar ComposeView
en tu diseño XML como cualquier otro elemento 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!" />
<androidx.compose.ui.platform.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, establece la estrategia de composición que mejor se adapte a la View
del host y llama a setContent()
a fin de usar Compose.
class ExampleFragment : Fragment() {
private var _binding: FragmentExampleBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentExampleBinding.inflate(inflater, container, false)
val view = binding.root
binding.composeView.apply {
// Dispose of the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
// In Compose world
MaterialTheme {
Text("Hello Compose!")
}
}
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Figura 1: Muestra el resultado del código que agrega elementos de Compose a una jerarquía de la IU de View. 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, por lo que no necesitas usar un archivo de diseño XML.
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
// Dispose of the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
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.
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>
View 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
.
Además, te permite volver a usar vistas personalizadas que ya hayas diseñado.
Para incluir un elemento o una jerarquía de vistas, usa el elemento AndroidView
que admite composición.
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 vista AndroidView
se recompone cada vez que cambia una lectura de State
dentro de la devolución de llamada. AndroidView
, como muchos otros elementos integrados que admiten composición, toma un parámetro Modifier
que se puede utilizar, por ejemplo, para definir su posición en el elemento superior que admite composición.
@Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// Adds view to Compose
AndroidView(
modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
factory = { context ->
// Creates custom view
CustomView(context).apply {
// Sets up listeners for View -> Compose communication
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
// View's been inflated or state read in this block has been updated
// 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.value
}
)
}
@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)
}
}
Fragmentos en Compose
Usa el elemento componible AndroidViewBinding
para agregar una Fragment
en Compose.
Para ello, aumenta un XML que contenga un FragmentContainerView
como contenedor de Fragment
.
Por ejemplo, si tienes el my_fragment_layout.xml
definido, puedes usar un código como este mientras reemplazas el atributo XML android:name
por el nombre de clase de tu Fragment
:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
Aumenta este fragmento en Compose de la siguiente manera:
@Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
Si necesitas usar varios fragmentos en el mismo diseño, asegúrate de haber definido un ID único para cada FragmentContainerView
.
Cómo llamar al framework de Android desde Compose
Compose funciona dentro de las clases del framework de Android. Por ejemplo, se aloja en clases de Android View, como Activity
o Fragment
, y es posible que necesite utilizar clases del framework de Android como Context
, recursos del sistema, Service
o BroadcastReceiver
.
Para obtener más información acerca de los recursos del sistema, consulta la documentación sobre recursos en Compose.
Configuraciones locales de composición
Las clases de CompositionLocal
permiten pasar datos de manera implícita por funciones que admiten composición. En general, tienen un valor en un nodo determinado del árbol de IU. Sus subordinados que admiten composición pueden utilizar ese valor sin declarar la CompositionLocal
como un parámetro en la función que admite composición.
CompositionLocal
se usa para propagar valores para los tipos de framework de Android en Compose como Context
, Configuration
o la View
en la que está alojado el código de Compose con los elementos LocalContext
, LocalConfiguration
o LocalView
.
Ten en cuenta que las clases CompositionLocal
tienen el prefijo Local
para darles mayor visibilidad con la función de autocompletar en el IDE.
Para acceder al valor actual de una CompositionLocal
, usa su propiedad current
. Por ejemplo, si llamas a LocalContext.current
, el siguiente código crea una vista personalizada con el Context
disponible en esa parte del árbol de IU de Compose.
@Composable
fun rememberCustomView(): CustomView {
val context = LocalContext.current
return remember { CustomView(context).apply { /*...*/ } }
}
Para obtener un ejemplo más completo, consulta la sección Caso de éxito: BroadcastReceivers al final de este documento.
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 : ComponentActivity() {
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)
}
}
Caso de éxito: BroadcastReceivers
Si deseas ver un ejemplo de funciones más realista, quizás te convenga migrar o implementar en Compose. Para mostrar CompositionLocal
y los efectos secundarios, es necesario registrar un BroadcastReceiver
, por ejemplo, desde una función que admite composición.
La solución emplea LocalContext
para usar el contexto actual, además de los efectos secundarios rememberUpdatedState
y DisposableEffect
.
@Composable
fun SystemBroadcastReceiver(
systemAction: String,
onSystemEvent: (intent: Intent?) -> Unit
) {
// Grab the current context in this part of the UI tree
val context = LocalContext.current
// Safely use the latest onSystemEvent lambda passed to the function
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
// If either context or systemAction changes, unregister and register again
DisposableEffect(context, systemAction) {
val intentFilter = IntentFilter(systemAction)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
currentOnSystemEvent(intent)
}
}
context.registerReceiver(broadcast, intentFilter)
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
}
@Composable
fun HomeScreen() {
SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
val isCharging = /* Get from batteryStatus ... */ true
/* Do something if the device is charging */
}
/* Rest of the HomeScreen */
}