API d'interopérabilité

Lors de l'adoption de Compose dans votre application, les interfaces utilisateur basées sur Compose et les vues peuvent être combinées. Voici une liste d'API, de recommandations et de conseils pour faciliter la transition vers Compose.

Compose dans les vues

Vous pouvez ajouter une UI basée sur Compose à une application existante qui utilise un design basé sur les vues.

Pour créer un écran entièrement basé sur Compose, demandez à votre activité d'appeler la méthode setContent() et de transmettre toutes les fonctions modulables de votre choix.

class ExampleActivity : ComponentActivity() {
    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!")
}

Ce code ressemble à ce que vous trouveriez dans une application Compose.

ViewCompositionStrategy pour ComposeView

Par défaut, Compose supprime la composition dès lors que la View est dissociée d'une fenêtre. Les types de View de l'interface utilisateur Compose, comme ComposeView et AbstractComposeView, utilisent un élément ViewCompositionStrategy qui définit ce comportement.

Par défaut, Compose utilise la stratégie DisposeOnDetachedFromWindowOrReleasedFromPool. Cependant, cette valeur par défaut est parfois indésirable, notamment lorsque les types de View de l'UI Compose sont utilisés dans les éléments suivants :

  • Les fragments. Pour conserver l'état, la composition doit suivre le cycle de vie de la vue du fragment pour les types de View de l'UI Compose.

  • Les transitions. Chaque fois que la View de l'UI Compose est utilisée dans le cadre d'une transition, elle est détachée de sa fenêtre au début de la transition et non à la fin : votre composable est alors supprimé alors qu'il s'affiche à l'écran.

  • Votre propre View personnalisée gérée par le cycle de vie.

Dans certaines de ces situations, l'application peut être sujette à des fuites de mémoire progressives au niveau des instances de composition, sauf si vous appelez AbstractComposeView.disposeComposition manuellement.

Vous pouvez supprimer automatiquement les compositions qui ne sont plus nécessaires en définissant une autre stratégie, ou en créant la vôtre en appelant la méthode setViewCompositionStrategy. Par exemple, la stratégie DisposeOnLifecycleDestroyed supprime la composition lorsque le lifecycle est détruit. Cette stratégie convient aux types View d'interface utilisateur Compose qui partagent une relation de type 1:1 avec un LifecycleOwner connu. Lorsque LifecycleOwner n'est pas connu, vous pouvez utiliser DisposeOnViewTreeLifecycleDestroyed.

Consultez ComposeView dans les fragments pour découvrir comment fonctionne cette API.

ComposeView dans les fragments

Si vous souhaitez intégrer le contenu de l'interface utilisateur Compose dans un fragment ou une mise en page "View" existante, utilisez ComposeView et appelez sa méthode setContent(). ComposeView est une View Android.

Vous pouvez placer la ComposeView dans votre mise en page XML comme n'importe quelle autre 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>

Dans le code source en Kotlin, gonflez la mise en page à partir de la ressource de mise en page définie en XML. Obtenez ensuite ComposeView à l'aide de l'ID XML, définissez la stratégie de composition la plus adaptée pour l'hôte View, puis appelez setContent() pour utiliser 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
        binding.composeView.apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

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

Deux éléments textuels légèrement différents, l'un au-dessus de l'autre.

Figure 1 : Cette image montre la sortie du code qui ajoute des éléments Compose dans une hiérarchie d'UI. Un widget TextView affiche le message texte "Hello Android!". Un élément de texte Compose affiche le texte "Hello Compose!".

Vous pouvez également inclure une ComposeView directement dans un fragment si votre mode plein écran est conçu avec Compose, ce qui vous évite d'utiliser un fichier de mise en page XML.

class ExampleFragmentNoXml : Fragment() {

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

Plusieurs ComposeView dans la même mise en page

S'il existe plusieurs éléments ComposeView dans la même mise en page, chacun doit disposer d'un ID unique pour que savedInstanceState fonctionne correctement.

class ExampleFragmentMultipleComposeView : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View = LinearLayout(requireContext()).apply {
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_x
                // ...
            }
        )
        addView(TextView(requireContext()))
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_y
                // ...
            }
        )
    }
}

Les ID ComposeView sont définis dans le fichier res/values/ids.xml :

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

Vues dans Compose

Vous pouvez inclure une hiérarchie des vues Android dans une interface utilisateur Compose. Cette approche est particulièrement utile si vous souhaitez utiliser des éléments d'interface utilisateur qui ne sont pas encore disponibles dans Compose, comme AdView. Cela vous permet également de réutiliser des vues personnalisées.

Pour inclure un élément ou une hiérarchie de vues, utilisez le composable AndroidView. AndroidView reçoit un lambda qui renvoie une View. AndroidView fournit également un rappel update, qui est appelé lorsque la vue est gonflée. AndroidView se recompose chaque fois qu'une lecture State du rappel change. Comme de nombreux autres composables intégrés, AndroidView utilise un paramètre Modifier qui peut servir, par exemple, à définir sa position dans le composable parent.

@Composable
fun CustomView() {
    var selectedItem by remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates view
            MyView(context).apply {
                // Sets up listeners for View -> Compose communication
                setOnClickListener {
                    selectedItem = 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.selectedItem = selectedItem
        }
    )
}

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

Pour intégrer une mise en page XML, utilisez l'API AndroidViewBinding fournie par la bibliothèque androidx.compose.ui:ui-viewbinding. Pour ce faire, votre projet doit activer la liaison de vue.

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

Fragments dans Compose

Utilisez le composable AndroidViewBinding pour ajouter un Fragment dans Compose. AndroidViewBinding offre une gestion spécifique au fragment, comme la suppression du fragment lorsque le composable quitte la composition.

Pour ce faire, gonflez le code XML d'un conteneur FragmentContainerView pour en faire le conteneur de votre Fragment.

Par exemple, si vous avez défini my_fragment_layout.xml, vous pouvez utiliser un code comme celui-ci tout en remplaçant l'attribut XML android:name par le nom de classe de Fragment :

<androidx.fragment.app.FragmentContainerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragment_container_view"
  android:layout_height="match_parent"
  android:layout_width="match_parent"
  android:name="com.example.MyFragment" />

Pour gonfler ce fragment dans Compose, procédez comme suit :

@Composable
fun FragmentInComposeExample() {
    AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
        val myFragment = fragmentContainerView.getFragment<MyFragment>()
        // ...
    }
}

Si vous devez utiliser plusieurs fragments dans la même mise en page, assurez-vous d'avoir défini un ID unique pour chaque FragmentContainerView.

Appeler le framework Android à partir de Compose

Compose fonctionne dans les classes du framework Android. Par exemple, il est hébergé sur les classes View Android, comme Activity ou Fragment, et peut avoir besoin d'utiliser des classes du framework Android comme Context, les ressources système, Service ou encore BroadcastReceiver.

Pour en savoir plus sur les ressources système, consultez la documentation Ressources disponibles dans Compose.

Compositions locales

Les classes CompositionLocal permettent de transmettre des données implicitement via des fonctions modulables. Elles sont généralement accompagnées d'une valeur dans un nœud spécifique de l'arborescence de l'interface utilisateur. Cette valeur peut être utilisée par ses descendants composables sans déclarer le CompositionLocal en tant que paramètre dans la fonction modulable.

CompositionLocal permet de propager des valeurs pour les types de frameworks Android dans Compose, tels que Context, Configuration ou View, dans lesquels le code Compose est hébergé avec les éléments LocalContext, LocalConfiguration ou LocalView correspondants. Notez que les classes CompositionLocal sont précédées de Local pour une meilleure visibilité avec la saisie semi-automatique dans l'IDE.

Pour accéder à la valeur actuelle de CompositionLocal, utilisez sa propriété current. Par exemple, le code ci-dessous affiche un toast en fournissant le LocalContext.current dans la méthode Toast.makeToast.

@Composable
fun ToastGreetingButton(greeting: String) {
    val context = LocalContext.current
    Button(onClick = {
        Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
    }) {
        Text("Greet")
    }
}

Pour un exemple plus complet, consultez la section Étude de cas : BroadcastReceivers à la fin de ce document.

Autres interactions

Si aucun utilitaire n'est défini pour l'interaction dont vous avez besoin, nous vous recommandons de suivre les consignes générales de Compose : le flux de données descend, le flux d'événements monte. Plus de détails sont disponibles dans Raisonnement dans Compose. Par exemple, ce composable lance une autre activité :

class OtherInteractionsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(Intent(this, MyActivity::class.java))
                })
            }
        }
    }
}

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

Étude de cas : BroadcastReceivers

Pour obtenir un exemple plus concret des fonctionnalités que vous pouvez migrer ou implémenter dans ComposeCompositionLocal, ainsi que des effets secondaires, imaginons qu'un BroadcastReceiver doit être enregistré à partir d'une fonction modulable.

La solution utilise LocalContext pour utiliser le contexte actuel, ainsi que les effets secondaires rememberUpdatedState et 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?) {
                currentOnSystemEvent(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 */
}