Interoperabilidade do Compose

O Jetpack Compose foi projetado para funcionar com a abordagem estabelecida de IU com base em visualização. Se você estiver criando um app, a melhor opção pode ser implementar toda a IU com o Compose. No entanto, se você estiver modificando um app já existente, talvez não queira migrá-lo totalmente de uma só vez. Em vez disso, você pode combinar o Compose com a implementação de design da IU existente.

Como adotar o Compose no seu app

Há duas maneiras principais de integrar o Compose com uma IU com base em visualização:

  • É possível adicionar elementos do Compose à IU já existente, seja criando uma tela totalmente nova com base no Compose ou adicionando elementos do Compose a uma atividade, fragmento ou layout de visualização existentes.

  • É possível adicionar um elemento de IU com base em visualização às funções que podem ser compostas. Isso permite adicionar visualizações do Android a um design baseado no Compose.

A migração do app inteiro para o Compose funciona melhor se feita passo a passo, com a granularidade de que o projeto precisa. É possível migrar uma tela ou até mesmo um fragmento ou qualquer outro elemento de IU reutilizável por vez. É possível usar várias abordagens diferentes:

  • A abordagem bottom-up começa a migrar os elementos menores da IU na tela, como um Button ou uma TextView, seguidos pelos elementos ViewGroup até que tudo seja convertido em funções que podem ser compostas.

  • A abordagem top-down começa a migrar os fragmentos ou contêineres de visualização, como FrameLayout, ConstraintLayout ou RecyclerView, seguidos pelos elementos de IU menores na tela.

Essas abordagens pressupõem que cada tela esteja autônoma, mas também é possível migrar a IU compartilhada, como um sistema de design, para o Jetpack Compose. Consulte abaixo Como migrar a IU compartilhada para saber mais.

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.

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 é um View para Android. É necessário anexar a ComposeView a uma ViewTreeLifecycleOwner. ViewTreeLifecycleOwner permite que a visualização seja anexada e desanexada repetidamente enquanto a composição é preservada. ComponentActivity, FragmentActivity e AppCompatActivity são exemplos de classes que implementam ViewTreeLifecycleOwner.

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 e chame setContent() para usar o Compose.

class ExampleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        return inflater.inflate(
            R.layout.fragment_example, container, false
        ).apply {
            findViewById<ComposeView>(R.id.compose_view).setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
    }
}

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 {
            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. Há mais informações sobre isso na seção SavedInstanceState.

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 compostos integrados, aceita um parâmetro Modifier que pode ser usado, por exemplo, para definir a posição dele no elemento 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)
    }
}

Integração com bibliotecas comuns

Para ver como o Compose é integrado a bibliotecas comuns, como ViewModel, Flow, Paging ou Hilt, confira o Guia de integração do Compose com bibliotecas comuns.

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 {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Consulte a documentação da biblioteca MDC para saber mais.

Biblioteca AppCompat Compose Theme Adapter

A biblioteca AppCompat Compose Theme Adapter permite reutilizar facilmente temas XML AppCompat para a definição de temas no Jetpack Compose. Ela cria um MaterialTheme com os valores de cor e tipografia usando o tema do contexto.

import com.google.accompanist.appcompattheme.AppCompatTheme

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( /* ... */)
        }
    }
}

Animação mostrando um elemento da IU rolando para cima e para baixo para dar lugar ao teclado.

Figura 2. Animações do IME usando a biblioteca accompanist-insets.

Consulte a documentação da biblioteca accompanists-insets para saber mais.

Como gerenciar mudanças no tamanho da tela

Ao migrar um app que usa layouts XML diferentes, dependendo do tamanho da tela, use o elemento BoxWithConstraints para saber o tamanho mínimo e máximo que um composto pode ocupar.

@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 */
        }
    }
}

Fonte da verdade da arquitetura e do estado

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 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 ViewModel em diferentes elementos 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 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 compostos na atividade do host. A primeira instância ViewModel criada é reutilizada em outros elementos 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
}

Como os gráficos de navegação também incluem o escopo de elementos ViewModel, os elementos 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 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 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 mais informações sobre efeitos colaterais, consulte a documentação de ciclo de vida e 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 composto TextField. O 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
    }
}

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 */ }
        }
    }
}

Caso o componente personalizado tenha um estado mutável, confira a seção de fonte da verdade do estado acima.

Teste

É possível testar o código combinado da visualização e do Compose usando a API createAndroidComposeRule(). Para ver mais informações, consulte Como testar o layout do Compose.

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 */
}