Como integrar o Compose à arquitetura de app que você já usa

Os padrões da arquitetura do fluxo de dados unidirecional (UDF, na sigla em inglês) funcionam perfeitamente com o Compose. Caso o app use outros tipos de padrão de arquitetura, como o Model View Presenter (MVP), recomendamos migrar essa parte da IU para a arquitetura UDF antes ou durante a adoção do Compose.

ViewModels no Compose

Se você usar a biblioteca Architecture Components ViewModel, poderá acessar um ViewModel em qualquer elemento que possa ser composto chamando a função viewModel(), conforme explicado na documentação da integração do Compose com bibliotecas comuns.

Ao adotar o Compose, tenha cuidado ao usar o mesmo tipo de ViewModel em diferentes elementos que podem ser compostos, considerando que os elementos ViewModel seguem os escopos do ciclo de vida da visualização. O escopo será a atividade do host, o fragmento ou o gráfico de navegação se a biblioteca Navigation for usada.

Por exemplo, se os elementos que podem ser compostos forem hospedados em uma atividade, o viewModel() sempre retornará a mesma instância que só será limpa quando a atividade for concluída. No exemplo a seguir, o mesmo usuário será recebido duas vezes, porque a mesma instância de GreetingViewModel será reutilizada em todos os elementos que podem ser compostos na atividade do host. A primeira instância ViewModel criada é reutilizada em outros elementos que podem ser compostos.

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 os gráficos de navegação também incluem o escopo de elementos ViewModel, os elementos que podem ser compostos que são um destino em um gráfico de navegação têm uma instância diferente do ViewModel. Nesse caso, o escopo do ViewModel é definido como o ciclo de vida do destino e será apagado quando o destino for removido da backstack. No exemplo a seguir, quando o usuário navega para a tela Profile, uma nova instância do GreetingViewModel é criada.

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

Fonte da verdade do estado

Quando você adota o Compose em uma parte da IU, é possível que o código do Compose e do sistema de visualização precisem compartilhar dados. Quando possível, recomendamos encapsular esse estado compartilhado em outra classe que siga as práticas recomendadas de UDF usadas pelas duas plataformas, como em um ViewModel que expõe um stream dos dados compartilhados para emitir atualizações de dados.

No entanto, isso nem sempre é possível se os dados a serem compartilhados forem mutáveis ou estiverem estreitamente vinculados a um elemento da IU. Nesse caso, um sistema precisa ser a fonte da verdade. Ele também precisa compartilhar as atualizações de dados com o outro sistema. Como regra geral, a fonte da verdade precisa ser de propriedade do elemento que estiver mais próximo da raiz da hierarquia da IU.

Compose como a fonte da verdade

Use o elemento que pode ser composto SideEffect para publicar o estado do Compose em um código que não seja dele. Nesse caso, a fonte da verdade é mantida em um elemento que pode ser composto que envia atualizações de estado.

Por exemplo, um OnBackPressedCallback precisa ser registrado para detectar o botão "Voltar" sendo pressionado em um OnBackPressedDispatcher. Para comunicar se o callback precisa ser ativado ou não, use SideEffect para atualizar o valor dele.

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

Para ver mais informações, consulte a documentação sobre Efeitos colaterais.

Sistema de visualização como fonte da verdade

Se o sistema de visualização é proprietário do estado e o compartilha com o Compose, recomendamos que você una o estado em objetos mutableStateOf para torná-lo seguro para linhas de execução no Compose. Se você usar essa abordagem, as funções compostas serão simplificadas, porque não terão mais a fonte da verdade. Mas o sistema de visualização precisará atualizar o estado imutável e as visualizações que usam esse estado.

No exemplo a seguir, um CustomViewGroup contém uma TextView e uma ComposeView com um elemento que pode ser composto TextField. A TextView precisa mostrar o conteúdo digitado pelo usuário no 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
    }
}