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 migrar tu app por completo de una sola vez. En su lugar, puedes combinar Compose con tu implementación de diseño de IU existente.

Cómo implementar Compose en tu app

Existen dos maneras principales de integrar 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 agregar elementos de Compose a un diseño de actividades, vistas o fragmento existente.

  • Puedes agregar un elemento de la IU basada en vistas a tus funciones que admiten composición. Esto te permite agregar vistas de Android a un diseño basado en Compose.

La mejor manera de migrar toda la app a Compose es hacerlo paso a paso con el nivel de detalle que necesita el proyecto. Puedes migrar una pantalla a la vez, o incluso un fragmento o cualquier otro elemento de la IU reutilizable a la vez. Puedes usar varios enfoques diferentes:

  • El enfoque desde abajo hacia arriba comienza migrando los elementos de la IU más pequeños en la pantalla, como Button o TextView, seguidos por sus elementos de ViewGroup, hasta que todos los elementos se hayan convertido en funciones que admiten composición.

  • El enfoque desde arriba hacia abajo comienza migrando los fragmentos o contenedores de vistas, como FrameLayout, ConstraintLayout o RecyclerView, seguidos por los elementos de la IU más pequeños en la pantalla.

Esos enfoques suponen que cada pantalla es independiente, pero también es posible migrar la IU compartida, como un sistema de diseño, a Jetpack Compose. Consulta Cómo migrar la IU compartida a continuación para obtener más información.

API de interoperabilidad

Cuando implementas Compose en tu app, puedes combinar IU basadas en vistas y de Compose. A continuación, se muestra una lista de API, recomendaciones y sugerencias para facilitar la transición a 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 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 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 vista 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 {
            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 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.

@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.

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 AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

Cómo llamar al framework de Android desde Compose

Compose está estrechamente vinculado a 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 mediante 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 Cómo pensar en 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

Para ver cómo se integra Compose con bibliotecas comunes como ViewModel, Flow, Paging o Hilt, consulta la guía de integración con bibliotecas comunes de Compose.

Temas

Según Material Design, el uso de la biblioteca Componentes de Material Design para Android (MDC) es la manera recomendada de diseñar apps para Android. Como se explica en la documentación de temas de Compose, Compose implementa esos conceptos con el elemento que admite composición MaterialTheme.

Cuando crees pantallas nuevas en Compose, asegúrate de aplicar un MaterialTheme antes de cualquier elemento que admita composición que emita IU desde la biblioteca de componentes materiales. Los componentes materiales (Button, Text, etc.) dependen de que se implemente un MaterialTheme, y su comportamiento no está definido sin él.

Todos los ejemplos de Jetpack Compose usan un tema de Compose personalizado creado sobre la base de MaterialTheme.

Varias fuentes de confianza

Es probable que una app existente tenga una gran cantidad de temas y estilos para las vistas. Si implementas Compose en una app existente, necesitarás migrar el tema a fin de usar MaterialTheme para cualquier pantalla de Compose. Eso significa que el tema de tu app tendrá 2 fuentes de confianza: un tema basado en la vista y otro basado en Compose. Si decides realizar cambios en tu estilo, deberás hacerlos en varios lugares.

Si tu plan es migrar por completo la app a Compose, eventualmente deberás crear una versión de Compose del tema existente. El problema es que, cuanto antes en el proceso de desarrollo crees tu tema de Compose, más mantenimiento tendrás que hacer durante el proceso.

Adaptador de temas de MDC Compose

Si utilizas la biblioteca de MDC en tu app para Android, la biblioteca del Adaptador de temas de MDC Compose te permitirá volver a usar fácilmente el color, la tipografía y la forma de tus temas basados en View existentes en tus elementos que admiten composición.

import com.google.android.material.composethemeadapter.MdcTheme

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

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Consulta la documentación sobre la biblioteca de MDC para obtener más información.

Adaptador de temas de AppCompat Compose

La biblioteca del Adaptador de temas de AppCompat Compose te permite volver a usar fácilmente temas de XML de AppCompat para la creación de temas en Jetpack Compose. Crea un MaterialTheme con los valores de color y tipografía del tema del contexto.

import com.google.accompanist.appcompattheme.AppCompatTheme

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

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Estilos de componentes predeterminados

Las bibliotecas de MDC y del Adaptador de temas de AppCompat Compose no leen ningún estilo de widget predeterminado definido por temas. Eso se debe a que Compose no tiene el concepto de elementos que admiten composición predeterminados.

Obtén más información sobre los estilos de componentes y los sistemas de diseño personalizados en la documentación de temas.

Superposiciones de temas en Compose

Cuando migres pantallas basadas en View a Compose, presta atención a los usos del atributo android:theme. Es probable que necesites un nuevo MaterialTheme en esa parte del árbol de IU de Compose.

Obtén más información al respecto en la guía sobre temas.

Animaciones de WindowInsets e IME

Puedes controlar WindowInsets si usas la biblioteca de inserciones de acompañamiento, que brinda elementos que admiten composición y modificadores para controlar las inserciones en tus diseños, además de compatibilidad con las animaciones IME.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Animación que muestra un elemento de IU que se desplaza desde arriba hacia abajo a fin de dejar lugar para un teclado.

Figura 2: Animaciones IME que usan la biblioteca de inserciones de acompañamiento

Si deseas obtener más información, consulta la documentación de la biblioteca de inserciones de acompañamiento.

Cómo controlar cambios de tamaños de pantalla

Si migras una app que usa distintos diseños de XML según el tamaño de la pantalla, utiliza el elemento que admite composición BoxWithConstraints para conocer el tamaño mínimo y máximo que puede ocupar un elemento que admite composición.

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

Fuente de confianza de estado y arquitectura

Los patrones de arquitectura de flujo unidireccional de datos (UDF) funcionan perfectamente con Compose. Si la app usa otros tipos de patrones de arquitectura, como Model View Presenter (MVP), te recomendamos que migres esa parte de la IU a UDF antes de implementar Compose o mientras lo haces.

ViewModels en Compose

Cuando utilizas la biblioteca de ViewModel de componentes de la arquitectura, puedes acceder a un ViewModel desde cualquier elemento que admite composición llamando a la función viewModel(), como se explica en la documentación sobre la integración de Compose con bibliotecas comunes.

Si implementas Compose, ten cuidado con usar el mismo tipo de ViewModel en distintos elementos que admiten composición, ya que los elementos ViewModel tienen alcances de ciclo de vida de View. Si se utiliza la biblioteca de navegación, el alcance será la actividad del host, el fragmento o el gráfico de navegación.

Por ejemplo, si los elementos que admiten composición están alojados en una actividad, viewModel() siempre mostrará la misma instancia, que recién se borrará cuando termine la actividad. En el siguiente ejemplo, se le dará la bienvenida al mismo usuario dos veces porque la misma instancia de GreetingViewModel se volvió a usar en todos los elementos que admiten composición dentro de la actividad de host. La primera instancia de ViewModel creada se vuelve a utilizar en otros elementos que admiten composición.

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

        setContent {
            MaterialTheme {
                Column {
                    Greeting("user1")
                    Greeting("user2")
                }
            }
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

Como los gráficos de navegación también definen el alcance de los elementos ViewModel, los elementos que admiten composición que sean un destino en un gráfico de navegación tendrán una instancia distinta del ViewModel. En este caso, el alcance de ViewModel se define según el ciclo de vida del destino, y se borrará cuando se quite el destino de la pila de actividades. En el siguiente ejemplo, cuando el usuario navega a la pantalla de perfil, se crea una nueva instancia de GreetingViewModel.

@Composable
fun MyScreen() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            Greeting(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

El estado como fuente de confianza

Si implementas Compose en una parte de la IU, es posible que Compose y el código de sistema de View necesiten compartir datos. Cuando sea posible, recomendamos encapsular ese estado compartido en otra clase que cumpla las prácticas recomendadas de UDF que utilizan las dos plataformas; por ejemplo, en un ViewModel que exponga un flujo de los datos compartidos para emitir actualizaciones de datos.

Sin embargo, no siempre es posible si los datos que se compartirán son mutables o si están estrechamente vinculados a un elemento de la IU. En ese caso, un sistema debe ser la fuente de confianza. Además, ese sistema debe compartir todas las actualizaciones de datos con el otro sistema. Como regla general, la fuente de confianza debe ser propiedad del elemento que esté más cerca de la raíz en la jerarquía de IU.

Compose como fuente de confianza

Usa el elemento que admite composición SideEffect para convertir el estado basado en Compose en código no basado en Compose. En este caso, la fuente de confianza se conserva en un elemento que admite composición y que envía actualizaciones de estado.

Por ejemplo, un elemento OnBackPressedCallback debe estar registrado para escuchar cuando se presione el botón Atrás en un OnBackPressedDispatcher. Para comunicar si la devolución de llamada debería activarse o no, usa SideEffect a fin de actualizar su valor.

@Composable
fun BackHandler(
    enabled: Boolean,
    backDispatcher: OnBackPressedDispatcher,
    onBack: () -> Unit
) {

    // Safely update the current `onBack` lambda when a new one is provided
    val currentOnBack by rememberUpdatedState(onBack)

    // Remember in Composition a back callback that calls the `onBack` lambda
    val backCallback = remember {
        // Always intercept back events. See the SideEffect for a more complete version
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // On every successful composition, update the callback with the `enabled` value
    // to tell `backCallback` whether back events should be intercepted or not
    SideEffect {
        backCallback.isEnabled = enabled
    }

    // If `backDispatcher` changes, dispose and reset the effect
    DisposableEffect(backDispatcher) {
        // Add callback to the backDispatcher
        backDispatcher.addCallback(backCallback)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            backCallback.remove()
        }
    }
}

Si deseas obtener más información sobre los efectos secundarios, consulta la documentación sobre ciclo de vida y efectos secundarios.

El sistema de vistas como fuente de confianza

Si el sistema de vistas es propietario del estado y lo comparte con Compose, te recomendamos que unas el estado de objetos mutableStateOf para que sea seguro a nivel de los subprocesos para Compose. Si usas este enfoque, las funciones que admiten composición se simplifican debido a que ya no tienen la fuente de confianza. Sin embargo, el sistema de View necesita actualizar el estado mutable y las vistas que usan ese estado.

En el siguiente ejemplo, un CustomViewGroup contiene una TextView y una ComposeView con un elemento que admite composición TextField en su interior. La TextView necesita mostrar el contenido de lo que escribe el usuario en el TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

Cómo migrar la IU compartida

Si migras gradualmente a Compose, es posible que necesites usar elementos compartidos de la IU en el sistema de Compose y de View. Por ejemplo, si tu app tiene un componente CallToActionButton personalizado, es posible que debas usarlo en pantallas basadas en Compose y en View.

En Compose, los elementos compartidos de la IU se convierten en elementos que admiten composición y que se pueden volver a usar en la app, independientemente de que el elemento al que se le aplica el estilo utilice XML o sea una vista personalizada. Por ejemplo, deberías crear un elemento CallToActionButton que admita composición para tu componente Button de llamada a la acción personalizada.

Para usar el elemento que admite composición en las pantallas basadas en View, debes crear un wrapper de vista personalizado que se extienda desde AbstractComposeView. En su elemento Content anulado que admite composición, coloca el elemento que creaste unido al tema de Compose como se muestra en el siguiente ejemplo:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Ten en cuenta que los parámetros que admiten composición se convierten en variables mutables dentro de la vista personalizada. Eso hace que la vista CallToActionViewButton personalizada aumente y se pueda usar (por ejemplo, con la vinculación de vistas), como una vista tradicional. Consulta el siguiente ejemplo:

class ExampleActivity : Activity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.something)
            onClick = { /* Do something */ }
        }
    }
}

Si el componente personalizado contiene un estado mutable, consulta la sección de fuente de confianza de estado más arriba.

Pruebas

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.

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?) {
                onSystemEvent(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 */
}