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 de Componentes da Arquitetura do ViewModel, vai poder acessar um ViewModel em qualquer elemento de composição 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 de composição, 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 de composição forem hospedados em uma atividade, o viewModel() sempre vai retornar a mesma instância que só será limpa quando a atividade for concluída. No exemplo abaixo, o mesmo usuário ("user1") é recebido duas vezes porque a mesma instância de GreetingViewModel é reutilizada em todos os elementos de composição na atividade do host. A primeira instância ViewModel criada é reutilizada em outros elementos de composição.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
) {
    val messageUser by viewModel.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 de composição que também 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 MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

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 combinável SideEffect para publicar o estado do Compose em um código que não seja dele. Nesse caso, a fonte da verdade é armazenada em um elemento que pode ser composto que envia atualizações de estado.

Por exemplo, sua biblioteca de análise pode permitir segmentar a população de usuários anexando metadados personalizados (nesse caso, propriedades do usuário) a todos os eventos de análise subsequentes. Para comunicar o tipo de usuário atual à biblioteca de análise, use o SideEffect para atualizar o valor da biblioteca.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

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 abaixo, um CustomViewGroup contém uma TextView e uma ComposeView com um elemento de composição 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
    }
}