Integra Compose con tu arquitectura de app existente

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
}

class GreetingViewModelFactory(private val userId: String): ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

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, consulta la documentación de 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. 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
    }
}