APIs de interoperabilidade

Ao adotar o Compose no seu app, as IUs do Compose e as baseadas em visualização podem ser combinadas. Veja uma lista de APIs, recomendações e dicas para facilitar a transição para o Compose.

Compose em visualizações do Android

É possível adicionar uma IU com base no Compose a um app já existente que usa um design com base em visualização.

Para criar uma tela nova e totalmente baseada no Compose, faça sua atividade chamar o método setContent() e transmitir as funções que podem ser compostas que você quer usar.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent { // In here, we can call composables!
            MaterialTheme {
                Greeting(name = "compose")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

Esse código é parecido com o que você encontraria em um app feito inteiramente com o Compose.

ViewCompositionStrategy para ComposeView

Por padrão, o Compose descarta a composição sempre que a visualização se desanexa de uma janela. Os tipos de View da IU do Compose, como ComposeView e AbstractComposeView, usam um ViewCompositionStrategy que define esse comportamento.

Por padrão, o Compose usa a estratégia DisposeOnDetachedFromWindow. No entanto, esse valor padrão pode ser indesejável em algumas situações em que os tipos de View da IU do Compose são usados em:

  • Fragmentos. A composição precisa seguir o ciclo de vida de visualização do fragmento para que os tipos de View da IU do Compose salvem o estado.

  • Transições. Sempre que a View da IU do Compose for usada como parte de uma transição, ela será removida da janela quando a transição for iniciada, e não quando terminar. Isso fará com que a função composta descarte seu estado enquanto ainda estiver na tela.

  • Fixadores de visualização da RecyclerView ou a própria View personalizada gerenciada por ciclo de vida.

Em algumas dessas situações, o app também poderá apresentar vazamento lento de memória nas instâncias de composição, a menos que você chame manualmente AbstractComposeView.disposeComposition.

Para descartar as composições automaticamente quando elas não forem mais necessárias, defina uma estratégia diferente ou crie uma própria chamando o método setViewCompositionStrategy. Por exemplo, a estratégia DisposeOnLifecycleDestroyed descarta a composição quando o lifecycle é destruído. Essa estratégia é adequada para os tipos deView da IU do Compose que compartilham uma relação de 1 para 1 com um LifecycleOwner conhecido. Quando o LifecycleOwner não for conhecido, o DisposeOnViewTreeLifecycleDestroyed poderá ser usado.

Veja essa API em ação na seção ComposeView em fragmentos.

ComposeView em fragmentos

Se você quiser incorporar o conteúdo da IU do Compose em um fragmento ou um layout de visualização já existente, use ComposeView e chame o método setContent() dele. ComposeView é uma View para Android.

Você pode colocar a ComposeView no seu layout XML como qualquer outra View:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

No código-fonte do Kotlin, infle o layout usando o recurso de layout definido no XML. Em seguida, acesse a ComposeView usando o ID do XML, defina uma estratégia de composição que funcione melhor para a View host e chame setContent() para usar o Compose.

class ExampleFragment : Fragment() {

    private var _binding: FragmentExampleBinding? = null
    // This property is only valid between onCreateView and onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentExampleBinding.inflate(inflater, container, false)
        val view = binding.root
        view.composeView.apply {
            // Dispose the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Dois elementos de texto um pouco diferentes, um acima do outro

Figura 1. Isso mostra a saída do código que adiciona elementos do Compose a uma hierarquia de IU de visualização. A mensagem "Hello Android!" é exibida por um widget TextView. A mensagem "Hello Compose!" é exibida por um elemento de texto do Compose.

Também será possível incluir uma ComposeView diretamente em um fragmento se a tela cheia for criada com o Compose, o que permite evitar totalmente o uso de um arquivo de layout XML.

class ExampleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            // Dispose the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }
}

Se houver vários elementos ComposeView no mesmo layout, cada um precisará ter um ID exclusivo para que savedInstanceState funcione.

class ExampleFragment : Fragment() {

  override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
          id = R.id.compose_view_x
          ...
      })
      addView(TextView(...))
      addView(ComposeView(...).apply {
          id = R.id.compose_view_y
          ...
      })
    }
  }
}

Os IDs ComposeView são definidos no arquivo res/values/ids.xml:

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

Visualizações do Android no Compose

É possível incluir uma hierarquia de visualização do Android em uma IU do Compose. Essa abordagem será útil principalmente se você quiser usar elementos da IU que ainda não estão disponíveis no Compose, como AdView ou MapView. Essa abordagem também permite reutilizar visualizações personalizadas que você pode ter criado.

Para incluir um elemento ou uma hierarquia de visualização, use a AndroidView que pode ser composta. AndroidView recebe um lambda que retorna um View. AndroidView também fornece um callback update que é chamado quando a visualização é inflada. O AndroidView faz a recomposição sempre que um State lido dentro do callback muda.

@Composable
fun CustomView() {
    val selectedItem = remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates custom view
            CustomView(context).apply {
                // Sets up listeners for View -> Compose communication
                myView.setOnClickListener {
                    selectedItem.value = 1
                }
            }
        },
        update = { view ->
            // View's been inflated or state read in this block has been updated
            // Add logic here if necessary

            // As selectedItem is read here, AndroidView will recompose
            // whenever the state changes
            // Example of Compose -> View communication
            view.coordinator.selectedItem = selectedItem.value
        }
    )
}

@Composable
fun ContentExample() {
    Column(Modifier.fillMaxSize()) {
        Text("Look at this CustomView!")
        CustomView()
    }
}
.

Para incorporar um layout XML, use a API AndroidViewBinding, que é fornecida pela biblioteca androidx.compose.ui:ui-viewbinding. Para isso, seu projeto precisa ativar a vinculação de visualizações.

O AndroidView, assim como muitos outros elementos que podem ser compostos integrados, aceita um parâmetro Modifier que pode ser usado, por exemplo, para definir a posição dele no elemento que pode ser composto pai.

@Composable
fun AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

Como chamar o framework do Android no Compose

O Compose está vinculado rigidamente às classes do framework do Android. Por exemplo, ele é hospedado em classes de visualização do Android, como Activity ou Fragment, e pode precisar usar classes do framework do Android como Context, recursos do sistema, Service ou BroadcastReceiver.

Para saber mais sobre recursos do sistema, consulte a documentação Recursos no Compose.

Classes Composition Locals

As classes CompositionLocal permitem passar dados implicitamente usando funções que podem ser compostas. Em geral, elas recebem um valor em determinado nó da árvore da IU. Esse valor pode ser usado pelos descendentes compostos sem declarar o CompositionLocal como um parâmetro na função composta.

O CompositionLocal é usado para propagar valores para tipos de framework do Android no Compose, como Context, Configuration ou View, em que o código do Compose é hospedado com o LocalContext, LocalConfiguration ou LocalView correspondente. As classes CompositionLocal têm o prefixo Local para melhor detecção do dispositivo com o preenchimento automático no ambiente de desenvolvimento integrado.

Acesse o valor atual de um CompositionLocal usando a propriedade current. Por exemplo, o código abaixo cria uma visualização personalizada usando o Context disponível nessa parte da árvore de IU do Compose chamando LocalContext.current.

@Composable
fun rememberCustomView(): CustomView {
    val context = LocalContext.current
    return remember { CustomView(context).apply { /*...*/ } }
}

Para um exemplo mais completo, confira a seção Estudo de caso: BroadcastReceivers no fim deste documento.

Outras interações

Caso não haja um utilitário definido para a interação necessária, a prática recomendada é seguir as diretrizes gerais do Compose, o fluxo de dados desce, os eventos sobem, discutidas com mais detalhes em Trabalhando com o Compose. Por exemplo, essa função que pode ser composta inicia uma atividade diferente:

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(/*...*/)
                })
            }
        }
    }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
    Button(onClick = onButtonClick) {
        Text(data.title)
    }
}

Estudo de caso: BroadcastReceivers

Para ver um exemplo mais realista dos recursos que você quer migrar ou implementar no Compose e para demonstrar o CompositionLocal e os efeitos colaterais, digamos que um BroadcastReceiver precise ser registrado usando uma função que pode ser composta.

A solução utiliza LocalContext para usar o contexto atual e os efeitos colaterais de rememberUpdatedState e DisposableEffect.

@Composable
fun SystemBroadcastReceiver(
    systemAction: String,
    onSystemEvent: (intent: Intent?) -> Unit
) {
    // Grab the current context in this part of the UI tree
    val context = LocalContext.current

    // Safely use the latest onSystemEvent lambda passed to the function
    val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)

    // If either context or systemAction changes, unregister and register again
    DisposableEffect(context, systemAction) {
        val intentFilter = IntentFilter(systemAction)
        val broadcast = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                onSystemEvent(intent)
            }
        }

        context.registerReceiver(broadcast, intentFilter)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            context.unregisterReceiver(broadcast)
        }
    }
}

@Composable
fun HomeScreen() {

    SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
        val isCharging = /* Get from batteryStatus ... */ true
        /* Do something if the device is charging */
    }

    /* Rest of the HomeScreen */
}