Se você tiver um app que usa uma IU com base em visualização, talvez não queira reescrever toda a IU de uma vez só. Esta página ajudará você a adicionar novos elementos do Compose à sua IU já existente.
Como migrar uma IU compartilhada
Caso você esteja migrando gradualmente para o Compose, talvez precise usar elementos de
IU compartilhados no Compose e no sistema de visualização. Por exemplo, caso seu app tenha um
componente CallToActionButton
personalizado, pode ser necessário usá-lo nas telas do Compose
e nas baseadas em visualizações.
No Compose, os elementos de IU compartilhados se tornam elementos compostos que podem ser reutilizados em
todo o app, independentemente de o elemento ser estilizado usando XML ou ser uma visualização personalizada. Por
exemplo, você pode criar um elemento CallToActionButton
composto para o componente Button
de chamada para ação personalizado.
Para usar esse elemento em telas baseadas em visualização, é necessário criar um
wrapper de visualização personalizado que se estenda de AbstractComposeView
. No
Content
substituído, coloque o elemento composto que você criou incluído no seu tema
do Compose, conforme mostrado no exemplo abaixo:
@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)
}
}
}
Observe que os parâmetros compostos se tornam variáveis mutáveis dentro da visualização
personalizada. Isso torna a visualização personalizada CallToActionViewButton
inflável e utilizável,
por exemplo, com vinculação de visualizações, assim como uma
visualização tradicional. Veja o exemplo abaixo:
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 */ }
}
}
}
Se o componente personalizado tiver um estado mutável, consulte a Fonte de verdade do estado.
Temas
Seguindo o Material Design, o uso da
biblioteca Material Design Components for
Android (MDC)
é a maneira recomendada de aplicar temas a apps Android. Conforme abordado na
documentação de temas no Compose, o Compose implementa
esses conceitos com a
função composta
MaterialTheme
.
Ao criar novas telas no Compose, aplique um
MaterialTheme
antes de elementos compostos que emitem a IU da biblioteca
Material Design Components. Os componentes do Material Design (Button
, Text
etc.) dependem da existência de um
MaterialTheme
, e o comportamento deles fica indefinido sem isso.
Todas as amostras do Jetpack Compose usam um
tema personalizado do Compose criado sobre
MaterialTheme
.
Várias fontes da verdade
Um app provavelmente tem uma grande quantidade de temas e estilos para
visualizações. Quando você introduz o Compose em um app existente, é necessário migrar o
tema para usar
MaterialTheme
em qualquer tela do Compose. Isso significa que os temas do seu app terão duas fontes da
verdade: o tema baseado na visualização e o tema do Compose. Qualquer mudança no estilo
precisa ser feita em vários lugares.
Se você pretende migrar o app totalmente para o Compose, é preciso criar uma versão do tema existente para ele. O problema é que, quanto antes no processo de desenvolvimento você criar o tema do Compose, mais manutenção você precisará fazer durante o desenvolvimento.
Biblioteca MDC Compose Theme Adapter
Se estiver usando a biblioteca MDC no seu app Android, a biblioteca MDC Compose Theme Adapter permitirá que você reutilize facilmente nas suas funções compostas os temas de cor, tipografia e forma presentes nos temas baseados na visualização existentes:
import com.google.android.material.composethemeadapter.MdcTheme
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Use MdcTheme instead of MaterialTheme
// Colors, typography, and shape have been read from the
// View-based theme used in this Activity
MdcTheme {
ExampleComposable(/*...*/)
}
}
}
}
Consulte a documentação da biblioteca MDC para saber mais.
Biblioteca AppCompat Compose Theme Adapter
A biblioteca AppCompat Compose Theme Adapter (link em inglês) permite reutilizar facilmente
temas XML da AppCompat para definir temas no
Jetpack Compose. Ela cria um
MaterialTheme
com os valores de cor e
tipografia (links em inglês) usando o
tema do contexto.
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 padrão dos componentes
As bibliotecas MDC e AppCompat Compose Theme Adapter não leem nenhum estilo de widget padrão definido pelo tema. Isso ocorre porque o Compose não tem o conceito de funções default compostas.
Saiba mais sobre estilos de componentes e sistemas de design personalizados na documentação de temas.
Sobreposições de tema no Compose
Ao migrar telas baseadas em visualização para o Compose, preste atenção aos usos do atributo
android:theme
. É provável que você precise de um novo
MaterialTheme
nessa parte da árvore de IU do Compose.
Leia mais sobre isso no Guia de temas.
Animações WindowInsets e IME
Você pode processar WindowInsets
usando a
biblioteca accompanist-insets,
que fornece elementos que podem ser compostos e modificadores para gerenciá-los nos seus layouts, além de
compatibilidade com animações
do 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( /* ... */)
}
}
}
Figura 2. Animações do IME usando a biblioteca accompanist-insets.
Consulte a documentação da biblioteca accompanists-insets para saber mais.
Priorizar a divisão de estado da apresentação
Tradicionalmente, uma View
tem um estado. Uma View
gerencia campos que
descrevem o que exibir, além de como exibir. Ao
converter uma View
para o Compose, separe os dados renderizados para
alcançar um fluxo de dados unidirecional, conforme explicado em mais detalhes na seção sobre elevação de estado.
Por exemplo, uma View
tem uma propriedade visibility
que descreve se ela está
visível, invisível ou se desapareceu. Essa é uma propriedade inerente da View
. Outras
partes do código podem mudar a visibilidade de uma View
, mas somente a própria View
realmente sabe qual é sua visibilidade atual. A lógica para garantir que
uma View
esteja visível pode ser propensa a erros e geralmente está vinculada à
View
em si.
Por outro lado, o Compose facilita a exibição de elementos que podem ser compostos totalmente diferentes usando a lógica condicional no Kotlin:
if (showCautionIcon) {
CautionIcon(/* ... */)
}
Por padrão, o CautionIcon
não precisa saber ou se importar por que está sendo exibido,
e não há o conceito de visibility
: ele está na composição
ou não.
Ao separar de maneira clara a lógica de gerenciamento de estados e apresentação, você pode mudar mais livremente a forma como o conteúdo é exibido como uma conversão de estado para a IU. A elevação do estado quando necessário também torna os elementos que podem ser compostos mais reutilizáveis, já que a propriedade do estado é mais flexível.
Promover componentes encapsulados e reutilizáveis
Os elementos da View
geralmente têm uma ideia do local em que residem: dentro de uma Activity
, uma
Dialog
, um Fragment
ou dentro de outra hierarquia de View
. Como eles
geralmente são inflados dos arquivos de layout estático, a estrutura geral de uma
View
tende a ser muito rígida. Isso resulta em um acoplamento rígido e dificulta a
alteração ou reutilização de uma View
.
Por exemplo, uma View
personalizada pode presumir que haja uma visualização filha de determinado
tipo com determinado ID e mudar as propriedades diretamente em resposta a uma
ação. Isso une os elementos da View
de forma rígida: a View
personalizada
poderá falhar ou ser corrompida se não encontrar a filha, que provavelmente não
poderá ser reutilizada sem a View
mãe personalizada.
Isso é um problema menor no Compose com os elementos que podem ser compostos reutilizáveis. Os pais podem especificar facilmente o estado e os callbacks para que elementos compostos reutilizáveis sejam programados sem a necessidade de saber exatamente onde serão usados.
var isEnabled by rememberSaveable { mutableStateOf(false) }
Column {
ImageWithEnabledOverlay(isEnabled)
ControlPanelWithToggle(
isEnabled = isEnabled,
onEnabledChanged = { isEnabled = it }
)
}
No exemplo acima, as três partes estão mais encapsuladas e menos acopladas:
A
ImageWithEnabledOverlay
só precisa saber qual é o estado atual deisEnabled
. Não é necessário saber que oControlPanelWithToggle
existe nem como pode ser controlado.O
ControlPanelWithToggle
não sabe que aImageWithEnabledOverlay
existe. Poderia haver zero, uma ou mais maneiras de exibirisEnabled
, e oControlPanelWithToggle
não precisaria ser mudado.Para o pai, não importa o nível de aninhamento de
ImageWithEnabledOverlay
ouControlPanelWithToggle
. Esses filhos podem animar mudanças, trocar ou transmitir conteúdo para outros filhos.
Esse padrão é conhecido como inversão de controle. Leia mais sobre isso na documentação de CompositionLocal
.
Como gerenciar mudanças no tamanho da tela
Ter recursos diferentes para tamanhos de janela diferentes é uma das principais maneiras de
criar layouts de View
responsivos. Embora os recursos qualificados ainda sejam uma opção
para decisões de layout na tela, o Compose facilita a mudança completa
de layouts em códigos com a lógica condicional normal. Usando ferramentas como
BoxWithConstraints
, é possível tomar decisões com base no espaço disponível para
elementos individuais, o que não é possível com recursos qualificados:
@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 */
}
}
}
Leia sobre como criar layouts adaptáveis para aprender as técnicas que o Compose oferece para criar IUs adaptáveis.
Rolagem aninhada com visualizações
Infelizmente, a rolagem aninhada entre o sistema de visualização e o Jetpack Compose ainda não está disponível. É possível verificar o andamento neste bug do Issue Tracker.
Compose na RecyclerView
O Jetpack Compose usa o DisposeOnDetachedFromWindow
como ViewCompositionStrategy
padrão.
Isso significa que a composição é descartada
sempre que a visualização é removida da janela.
Ao usar uma ComposeView
como parte de um armazenador de visualização da RecyclerView
, a estratégia padrão é ineficiente,
porque as instâncias de composição permanecerão na memória até a
RecyclerView
ser removida da janela. O descarte da composição
quando a ComposeView
não é mais necessária para a RecyclerView
é uma
prática recomendada.
A função disposeComposition
possibilita descartar manualmente a composição de uma
ComposeView
. Você pode chamar essa função quando a visualização é reciclada desta forma:
import androidx.compose.ui.platform.ComposeView
class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MyComposeViewHolder {
return MyComposeViewHolder(ComposeView(parent.context))
}
override fun onViewRecycled(holder: MyComposeViewHolder) {
// Dispose the underlying Composition of the ComposeView
// when RecyclerView has recycled this ViewHolder
holder.composeView.disposeComposition()
}
/* Other methods */
}
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
/* ... */
}
Para fazer o armazenador de visualização do Compose funcionar em todos os cenários, é necessário usar a estratégia
DisposeOnViewTreeLifecycleDestroyed
, conforme abordado na seção ViewCompositionStrategy para ComposeView do
Guia de APIs de interoperabilidade.
import androidx.compose.ui.platform.ViewCompositionStrategy
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bind(input: String) {
composeView.setContent {
MdcTheme {
Text(input)
}
}
}
}
Para ver a ComposeView
usada na RecyclerView
em ação, confira
a ramificação compose_recyclerview
do app Sunflower.