1. Introdução
O Compose e o sistema de visualização podem trabalhar lado a lado.
Neste codelab, você vai migrar partes da tela de detalhes das plantas do app Sunflower (link em inglês) para o Compose. Criamos uma cópia do projeto para você testar a migração de um app realista para o Compose.
Ao final do codelab, você vai poder continuar a migração e converter o restante das telas do Sunflower, se quiser.
Para receber mais suporte durante este codelab, confira as orientações neste vídeo (em inglês):
O que você vai aprender
Neste codelab, você vai aprender o seguinte:
- Os diferentes caminhos de migração que você pode seguir.
- Como migrar um app para o Compose gradualmente.
- Como adicionar o Compose a uma tela já criada com visualizações.
- Como usar uma visualização no Compose.
- Como criar um tema no Compose.
- Como testar uma tela mista criada com visualizações e com o Compose.
Pré-requisitos
- Experiência com a sintaxe do Kotlin, incluindo lambdas.
- Conhecimento das noções básicas do Compose.
O que é necessário
2. Estratégia de migração
O Jetpack Compose foi desenvolvido com interoperabilidade de visualização desde o início. Recomendamos que a migração para o Compose seja feita de modo incremental, em que esse sistema e as visualizações sejam usados juntos na base de código até que o app passe a usar o Compose totalmente.
A estratégia de migração recomendada é esta:
- Criar telas com o Compose.
- Ao criar recursos, identifique elementos reutilizáveis e comece a criar uma biblioteca de componentes de interface comuns.
- Substituir os recursos atuais uma tela por vez.
Criar telas com o Compose.
Usar o Compose para criar recursos que abrangem uma tela inteira é a melhor maneira de impulsionar a adoção desse kit de ferramentas. Com essa estratégia, você pode adicionar recursos e aproveitar os benefícios do Compose enquanto ainda satisfaz as necessidades de negócios da sua empresa.
Um novo recurso pode abranger uma tela inteira. Nesse caso, toda a tela estaria no Compose. Se você estiver usando a navegação baseada em fragmentos, um novo fragmento será criado e você terá o conteúdo dele no Compose.
Você também pode introduzir novos recursos em uma tela já existente. Nesse caso, as visualizações e o Compose vão coexistir na mesma tela. Por exemplo, digamos que o recurso que você está adicionando seja um novo tipo de visualização em uma RecyclerView. Nesse caso, o novo tipo de visualização ficaria no Compose, e os outros itens permaneceriam os mesmos.
Criar uma biblioteca de componentes de interface comuns
Ao criar recursos com o Compose, você vai acabar criando uma biblioteca de componentes rapidamente. Identifique componentes reutilizáveis e promova a reutilização deles o máximo possível no seu app para que os componentes compartilhados tenham uma única fonte de verdade. Os recursos que você cria podem depender dessa biblioteca.
Substituir recursos existentes pelo Compose
Além de criar novos recursos, você pode migrar gradualmente os recursos atuais do app para o Compose. Você decide como vai fazer isso. Confira algumas boas opções:
- Telas simples: telas simples no app e não muito dinâmicas com poucos elementos de interface, por exemplo, a tela de boas-vindas, uma tela de confirmação ou uma tela de configurações. Essas são boas opções para migrar para o Compose, já que é possível fazer isso com apenas algumas linhas de código.
- Telas mistas com visualizações e o Compose: telas que já contêm um pouco de código do Compose são outra boa opção, já que você pode continuar a migração dos elementos nessa tela por partes. Se você tem uma tela apenas com uma subárvore no Compose, pode continuar migrando outras partes da árvore até que toda a interface esteja no Compose. Essa é a abordagem de migração de baixo para cima.
A abordagem deste codelab
Neste codelab, você vai fazer uma migração gradual para o Compose na tela de detalhes da planta no app Sunflower, trabalhando com o Compose e as visualizações ao mesmo tempo. Depois disso, você poderá continuar a migração por conta própria, se quiser.
3. Etapas da configuração
Buscar o código
Acesse o código do codelab no GitHub:
$ git clone https://github.com/android/codelab-android-compose
Se preferir, faça o download do repositório como um arquivo ZIP:
Como executar o app de exemplo
Você fez o download de um código que contém todos os codelabs disponíveis do Compose. Para concluir este codelab, abra o projeto MigrationCodelab
no Android Studio.
Neste codelab, você vai migrar a tela de detalhes da planta do app Sunflower (link em inglês) para o Compose. Você pode abrir a tela de detalhes tocando em uma das plantas disponíveis na lista mostrada no app.
Configuração do projeto
O projeto é criado em várias ramificações Git:
- A ramificação
main
é o ponto de partida do codelab. end
contém a solução deste codelab.
Recomendamos que você comece com o código na ramificação main
e siga as etapas do codelab no seu ritmo.
Durante o codelab, você verá snippets de código que precisam ser adicionados ao projeto. Em alguns locais, também vai ser necessário remover o código que é explicitamente mencionado nos comentários dos snippets de código.
Para acessar a ramificação end
pelo git, use cd
para acessar o diretório do projeto MigrationCodelab
e use o comando:
$ git checkout end
Ou faça o download do código da solução aqui:
Perguntas frequentes
4. Compose no app Sunflower
O Compose já foi adicionado ao código que você transferiu por download da ramificação main
. No entanto, vamos dar uma olhada no que é necessário para que ele funcione.
Se você abrir o arquivo build.gradle
no nível do app, vai notar como ele importa as dependências do Compose e permite que o Android Studio funcione com ele usando a flag buildFeatures { compose true }
.
app/build.gradle
android {
//...
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
//...
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
dependencies {
//...
// Compose
def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
implementation(composeBom)
androidTestImplementation(composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
//...
}
A versão dessas dependências é definida no arquivo build.gradle
do projeto.
5. Olá, Compose!
Na tela de detalhes da planta, migraremos a descrição para o Compose, deixando a estrutura geral intacta.
O Compose precisa de uma atividade ou fragmento do host para renderizar a interface. No Sunflower, como todas as telas usam fragmentos, você vai usar a ComposeView
: uma visualização do Android que pode hospedar conteúdo da interface do Compose com o método setContent
.
Como remover o código XML
Vamos começar a migração. Abra fragment_plant_detail.xml
e faça o seguinte:
- Mude para a Visualização "Code".
- Remova o código da
ConstraintLayout
e as quatroTextView
s aninhadas naNestedScrollView
. O codelab compara e referencia o código XML ao migrar itens individuais. É útil adicionar comentários ao código. - Adicione uma
ComposeView
para hospedar o código do Compose em vez dacompose_view
como o ID da visualização.
fragment_plant_detail.xml
<androidx.core.widget.NestedScrollView
android:id="@+id/plant_detail_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_bottom_padding"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- Step 2) Comment out ConstraintLayout and its children –->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
<TextView
android:id="@+id/plant_detail_name"
...
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- End Step 2) Comment out until here –->
<!-- Step 3) Add a ComposeView to host Compose code –->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.core.widget.NestedScrollView>
Como adicionar código do Compose
Agora, você já pode migrar a tela de detalhes da planta para o Compose.
Ao longo do codelab, você vai adicionar um código do Compose ao arquivo PlantDetailDescription.kt
na pasta plantdetail
. Abra o arquivo e veja como já existe um texto "Hello Compose"
marcador de posição no projeto.
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
Vamos mostrar esse texto na tela chamando a função combinável da ComposeView
adicionada na etapa anterior. Abra PlantDetailFragment.kt
.
Como a tela está usando a vinculação de dados, você pode acessar diretamente a composeView
e chamar setContent
para exibir o código do Compose na tela. Chame a função combinável PlantDetailDescription
dentro do MaterialTheme
, já que o app Sunflower usa o Material Design.
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
// ...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
// ...
composeView.setContent {
// You're in Compose world!
MaterialTheme {
PlantDetailDescription()
}
}
}
// ...
}
}
Se você executar o app, a mensagem "Hello Compose
" vai aparecer na tela.
6. Como criar um elemento combinável fora do XML
Vamos começar migrando o nome da planta. Mais exatamente, a TextView
com o ID @+id/plant_detail_name
removido em fragment_plant_detail.xml
. Veja o código XML:
<TextView
android:id="@+id/plant_detail_name"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@{viewModel.plant.name}"
android:textAppearance="?attr/textAppearanceHeadline5"
... />
Veja como a TextView tem um estilo textAppearanceHeadline5
, uma margem horizontal de 8.dp
e é centralizada horizontalmente na tela. No entanto, o título exibido é observado em um LiveData
exposto pelo PlantDetailViewModel
da camada do repositório.
Como a observação de um LiveData
vai ser abordada mais tarde, vamos supor que o nome esteja disponível e seja transmitido como um parâmetro para um novo elemento combinável PlantName
no arquivo PlantDetailDescription.kt
. Esse elemento vai ser chamado mais tarde no PlantDetailDescription
.
PlantDetailDescription.kt
@Composable
private fun PlantName(name: String) {
Text(
text = name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
@Preview
@Composable
private fun PlantNamePreview() {
MaterialTheme {
PlantName("Apple")
}
}
Veja uma prévia
Em que:
- O estilo de
Text
éMaterialTheme.typography.headlineSmall
, que é semelhante atextAppearanceHeadline5
do código XML. - Os modificadores enfeitam o texto para que ele se pareça com a versão do XML:
- O modificador
fillMaxWidth
é usado para ocupar o valor máximo de largura disponível. Esse modificador corresponde ao valor dematch_parent
do atributolayout_width
no código XML. - O modificador
padding
é usado para que um valor de padding horizontal demargin_small
seja aplicado. Isso corresponde às declaraçõesmarginStart
emarginEnd
no XML. O valormargin_small
também é o recurso de dimensão atual que é buscado usando a função auxiliardimensionResource
. - O modificador
wrapContentWidth
é usado para alinhar o texto de modo que ele fique centralizado horizontalmente. Isso é semelhante a umagravity
decenter_horizontal
no XML.
7. ViewModels e LiveData
Agora, vamos conectar o título à tela. Para fazer isso, carregue os dados usando o PlantDetailViewModel
. O Compose vem com integrações para ViewModel e LiveData.
ViewModels
Como uma instância do PlantDetailViewModel
é usada no fragmento, ela pode ser transmitida como um parâmetro para PlantDetailDescription
.
Abra o arquivo PlantDetailDescription.kt
e adicione o parâmetro PlantDetailViewModel
a PlantDetailDescription
:
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
//...
}
Agora, transmita a instância do ViewModel ao chamar esse elemento combinável no fragmento:
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
...
composeView.setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
LiveData
Com isso, você já tem acesso ao campo LiveData<Plant>
do PlantDetailViewModel
para ver o nome da planta.
Para observar o LiveData em um elemento combinável, use a função LiveData.observeAsState()
.
Como os valores emitidos pelo LiveData podem ser null
, você vai precisar agrupar o uso em uma verificação null
. Por isso, e para reutilização, é melhor dividir o consumo de LiveData e detectar em diferentes elementos combináveis. Vamos criar um novo elemento combinável com o nome PlantDetailContent
, que vai mostrar informações de Plant
.
Com essas atualizações, o arquivo PlantDetailDescription.kt
ficará assim:
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
// Observes values coming from the VM's LiveData<Plant> field
val plant by plantDetailViewModel.plant.observeAsState()
// If plant is not null, display the content
plant?.let {
PlantDetailContent(it)
}
}
@Composable
fun PlantDetailContent(plant: Plant) {
PlantName(plant.name)
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
A PlantNamePreview
precisa refletir nossa mudança sem precisar de uma atualização direta, já que o PlantDetailContent
chama PlantName
:
Agora, você conectou o ViewModel para que um nome de planta seja mostrado no Compose. Nas próximas seções, você vai criar o restante dos elementos combináveis e os conectar ao ViewModel de maneira semelhante.
8. Mais migração de código XML
Ficou mais fácil preencher o que falta na nossa interface: as informações de irrigação e a descrição da planta. Seguindo uma abordagem semelhante à anterior, já é possível migrar o restante da tela.
O código XML das informações de irrigação removido de fragment_plant_detail.xml
consiste em duas TextViews com IDs plant_watering_header
e plant_watering
.
<TextView
android:id="@+id/plant_watering_header"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
android:text="@string/watering_needs_prefix"
android:textColor="?attr/colorAccent"
android:textStyle="bold"
... />
<TextView
android:id="@+id/plant_watering"
...
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:gravity="center_horizontal"
app:wateringText="@{viewModel.plant.wateringInterval}"
.../>
Assim como você fez antes, crie uma nova função combinável com o nome PlantWatering
e adicione elementos combináveis Text
para mostrar as informações de irrigação na tela:
PlantDetailDescription.kt
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
Column(Modifier.fillMaxWidth()) {
// Same modifier used by both Texts
val centerWithPaddingModifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.align(Alignment.CenterHorizontally)
val normalPadding = dimensionResource(R.dimen.margin_normal)
Text(
text = stringResource(R.string.watering_needs_prefix),
color = MaterialTheme.colorScheme.primaryContainer,
fontWeight = FontWeight.Bold,
modifier = centerWithPaddingModifier.padding(top = normalPadding)
)
val wateringIntervalText = pluralStringResource(
R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
)
Text(
text = wateringIntervalText,
modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
)
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MaterialTheme {
PlantWatering(7)
}
}
Veja uma prévia
Algumas coisas a observar:
- Como o padding horizontal e a decoração do alinhamento são compartilhados pelos elementos
Text
, você pode atribuir o modificador a uma variável local (por exemplo,centerWithPaddingModifier
) para o reutilizar. Isso é possível porque modificadores são objetos normais do Kotlin. - O
MaterialTheme
do Compose não tem uma correspondência exata para ocolorAccent
usado noplant_watering_header
. Por enquanto, useMaterialTheme.colorScheme.primaryContainer
. Vamos melhorar isso na seção de aplicação de temas de interoperabilidade. - No Compose 1. 2.1 Para usar o
pluralStringResource
é necessário ativar oExperimentalComposeUiApi
. Em uma versão futura do Compose, talvez isso não seja mais necessário.
Além disso, vamos conectar todas as partes e chamar PlantWatering
no PlantDetailContent
. O código XML ConstraintLayout que removemos no início tinha uma margem de 16.dp
que precisamos incluir no código do Compose.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/margin_normal">
Em PlantDetailContent
, crie uma Column
para mostrar o nome e as informações de irrigação e usar essas informações como padding. Além disso, para que as cores do plano de fundo e do texto sejam adequadas, adicione uma Surface
para processar essa informação.
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
}
}
}
Se você atualizar a visualização, vai ver o seguinte:
9. Visualizações no código do Compose
Agora, vamos migrar a descrição da planta. O código em fragment_plant_detail.xml
tinha uma TextView
com app:renderHtml="@{viewModel.plant.description}"
para informar ao XML qual texto mostrar na tela. renderHtml
é um adaptador de vinculação que pode ser encontrado no arquivo PlantDetailBindingAdapters.kt
. A implementação usa HtmlCompat.fromHtml
para definir o texto na TextView
.
No entanto, o Compose não oferece suporte a classes Spanned
nem à exibição de texto formatado em HTML. Sendo assim, precisamos usar uma TextView
do sistema de visualização no código do Compose para ignorar essa limitação.
Como o Compose ainda não pode renderizar o código HTML, você vai criar uma TextView
de maneira programática para fazer isso usando a API AndroidView
.
A AndroidView
permite criar uma View
na lambda factory
. Ela também fornece uma lambda update
, que é invocada quando a visualização é inflada e em recomposições seguintes.
Para isso, vamos criar um novo elemento PlantDescription
combinável. Esse elemento combinável chama AndroidView
, que constrói uma TextView
na lambda factory
. Na lambda factory
, inicialize uma TextView
que mostre texto formatado em HTML e que, em seguida, defina o movementMethod
como uma instância de LinkMovementMethod
. Por fim, na lambda update
, defina o texto da TextView
como htmlDescription
.
PlantDetailDescription.kt
@Composable
private fun PlantDescription(description: String) {
// Remembers the HTML formatted description. Re-executes on a new description
val htmlDescription = remember(description) {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
// Displays the TextView on the screen and updates with the HTML description when inflated
// Updates to htmlDescription will make AndroidView recompose and update the text
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
}
},
update = {
it.text = htmlDescription
}
)
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
MaterialTheme {
PlantDescription("HTML<br><br>description")
}
}
Veja uma prévia:
Observe que a htmlDescription
se lembra da descrição em HTML de uma determinada description
transmitida como parâmetro. Se o parâmetro description
mudar, o código htmlDescription
dentro de remember
vai ser executado novamente.
Como resultado, o callback de atualização AndroidView
vai ser recomposto se o htmlDescription
mudar. Qualquer estado lido dentro da lambda update
causa uma recomposição.
Vamos também adicionar uma PlantDescription
à função combinável PlantDetailContent
e mudar o código de visualização para mostrar uma descrição em HTML:
PlantDetailDescription.kt
@Composable
fun PlantDetailContent(plant: Plant) {
Surface {
Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
PlantName(plant.name)
PlantWatering(plant.wateringInterval)
PlantDescription(plant.description)
}
}
}
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
MaterialTheme {
PlantDetailContent(plant)
}
}
Veja uma prévia
Neste ponto, você migrou todo o conteúdo do ConstraintLayout
original para o Compose. Execute o app para ver se ele está funcionando como esperado.
10. ViewCompositionStrategy
O Compose descarta a composição sempre que a ComposeView
é removida de uma janela. Isso não é o ideal quando a ComposeView
é usada em fragmentos, por dois motivos:
- A composição precisa seguir o ciclo de vida de visualização do fragmento para que os tipos de
View
da interface do Compose salvem o estado. - Quando ocorrem transições, a
ComposeView
fica em um estado desconectado. No entanto, os elementos da interface do Compose ainda vão ficar visíveis durante essas transições.
Para modificar esse comportamento, chame setViewCompositionStrategy
com a ViewCompositionStrategy
adequada para que ela siga o ciclo de vida de visualização do fragmento. Mais especificamente, recomendamos usar a estratégia DisposeOnViewTreeLifecycleDestroyed
para descartar a composição quando o LifecycleOwner
do fragmento for destruído.
Como o PlantDetailFragment
tem transições de entrada e saída (consulte nav_garden.xml
para mais informações), e como usaremos tipos de View
no Compose, precisamos garantir que a ComposeView
use a estratégia DisposeOnViewTreeLifecycleDestroyed
. No entanto, a prática recomendada é sempre definir essa estratégia ao usar ComposeView
em fragmentos.
PlantDetailFragment.kt
import androidx.compose.ui.platform.ViewCompositionStrategy
...
class PlantDetailFragment : Fragment() {
...
override fun onCreateView(...): View? {
val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
inflater, R.layout.fragment_plant_detail, container, false
).apply {
...
composeView.apply {
// Dispose the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
...
}
}
11. Temas do Material Design
Migramos o conteúdo de texto dos detalhes da planta para o Compose. No entanto, talvez você tenha percebido que o Compose não está usando as cores certas do tema. O nome da planta, que deveria estar verde, está roxo.
Para usar as cores corretas do tema, é necessário personalizar o MaterialTheme
definindo seu próprio tema e fornecendo as cores.
Personalização do MaterialTheme
Para criar seu próprio tema, abra o arquivo Theme.kt
no pacote theme
. O Theme.kt
define um elemento combinável chamado SunflowerTheme
, que aceita uma lambda de conteúdo e a transmite para um MaterialTheme
.
Ele ainda não faz nada interessante. A personalização será feita a seguir.
Theme.kt
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun SunflowerTheme(
content: @Composable () -> Unit
) {
MaterialTheme(content = content)
}
O MaterialTheme
permite que você personalize as cores, a tipografia e as formas. Por enquanto, personalize as cores fornecendo as mesmas cores no tema do Sunflower View. O SunflowerTheme
também pode aceitar um parâmetro booleano darkTheme
, que vai assumir true
como padrão se o sistema estiver no modo escuro. Caso contrário, o valor será false
. Com esse parâmetro, podemos transmitir os valores de cor corretos ao MaterialTheme
para corresponder ao tema do sistema definido no momento.
Theme.kt
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColorScheme(
primary = colorResource(id = R.color.sunflower_green_500),
primaryContainer = colorResource(id = R.color.sunflower_green_700),
secondary = colorResource(id = R.color.sunflower_yellow_500),
background = colorResource(id = R.color.sunflower_green_500),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
)
val darkColors = darkColorScheme(
primary = colorResource(id = R.color.sunflower_green_100),
primaryContainer = colorResource(id = R.color.sunflower_green_200),
secondary = colorResource(id = R.color.sunflower_yellow_300),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
onBackground = colorResource(id = R.color.sunflower_black),
surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
onSurface = colorResource(id = R.color.sunflower_white),
)
val colors = if (darkTheme) darkColors else lightColors
MaterialTheme(
colorScheme = colors,
content = content
)
}
Para usar, substitua o MaterialTheme
pelo SunflowerTheme
. Por exemplo, no PlantDetailFragment
:
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
composeView.apply {
...
setContent {
SunflowerTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
E todos os elementos da visualização no arquivo PlantDetailDescription.kt
:
PlantDetailDescription.kt
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
@Preview
@Composable
private fun PlantNamePreview() {
SunflowerTheme {
PlantName("Apple")
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
SunflowerTheme {
PlantWatering(7)
}
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
SunflowerTheme {
PlantDescription("HTML<br><br>description")
}
}
Como é possível observar na visualização, as cores agora devem corresponder às do tema Sunflower.
Também é possível visualizar a interface no tema escuro criando uma nova função e transmitindo Configuration.UI_MODE_NIGHT_YES
ao uiMode
:
import android.content.res.Configuration
...
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
Veja uma prévia
Se você executar o app, ele vai se comportar da mesma forma que antes da migração, tanto no tema claro quanto no escuro:
12. Testes
Depois de migrar partes da tela de detalhes da planta para o Compose, é fundamental fazer os testes para garantir que tudo esteja funcionando bem.
No app Sunflower, o PlantDetailFragmentTest
localizado na pasta androidTest
testa algumas funcionalidades do app. Abra o arquivo e dê uma olhada no código atual:
testPlantName
verifica o nome da planta na tela.testShareTextIntent
verifica se a intent correta é acionada após um toque no botão "Share" (compartilhar).
Quando uma atividade ou um fragmento usa o Compose, em vez de usar a ActivityScenarioRule
, você precisa usar a createAndroidComposeRule
, que integra a ActivityScenarioRule
com uma ComposeTestRule
que possibilita testar o código do Compose.
Em PlantDetailFragmentTest
, substitua o uso de ActivityScenarioRule
por createAndroidComposeRule
. Quando a regra de atividade for necessária para configurar o teste, use o atributo activityRule
da createAndroidComposeRule
desta maneira:
@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {
@Rule
@JvmField
val composeTestRule = createAndroidComposeRule<GardenActivity>()
...
@Before
fun jumpToPlantDetailFragment() {
populateDatabase()
composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
activity = gardenActivity
val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
}
}
...
}
Se você executar os testes, o testPlantName
vai falhar. A função testPlantName
verifica se há uma TextView na tela. No entanto, você migrou essa parte da interface para o Compose. Então, é necessário usar as declarações do Compose:
@Test
fun testPlantName() {
composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}
Se você executar os testes, vai ver que todos são aprovados.
13. Parabéns
Parabéns, você concluiu este codelab.
A ramificação compose
(link em inglês) do projeto original do app Sunflower no GitHub migra completamente a tela de detalhes da planta para o Compose. Além do que você fez neste codelab, ela também simula o comportamento do CollapsingToolbarLayout. Isso envolve:
- Carregar imagens com o Compose
- Animações
- Processamento de dimensões aprimorado
- E muito mais
Qual é a próxima etapa?
Confira os outros codelabs no programa de aprendizagem do Compose:
Leia mais
- Orientações sobre o código de migração do Jetpack Compose
- Página de documentação Migrar apps baseados em visualização