Using Views in Compose

You can include an Android View hierarchy in a Compose UI. This approach is particularly useful if you want to use UI elements that are not yet available in Compose, like AdView. This approach also lets you reuse custom views you may have designed.

To include a view element or hierarchy, use the AndroidView composable. AndroidView is passed a lambda that returns a View. AndroidView also provides an update callback that is called when the view is inflated. The AndroidView recomposes whenever a State read within the callback changes. AndroidView, like many other built-in composables, takes a Modifier parameter that can be used, for example, to set its position in the parent composable.

@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()
    }
}

AndroidView with view binding

To embed an XML layout, use the AndroidViewBinding API, which is provided by the androidx.compose.ui:ui-viewbinding library. To do this, your project must enable view binding.

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

AndroidView in Lazy lists

If you are using an AndroidView in a Lazy list (LazyColumn, LazyRow, Pager, etc.), consider using the AndroidView overload introduced in version 1.4.0-rc01. This overload allows Compose to reuse the underlying View instance when the containing composition is reused as is the case for Lazy lists.

This overload of AndroidView adds 2 additional parameters:

  • onReset - A callback invoked to signal that the View is about to be reused. This must be non-null to enable View reuse.
  • onRelease (optional) - A callback invoked to signal that the View has exited the composition and will not be reused again.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun AndroidViewInLazyList() {
    LazyColumn {
        items(100) { index ->
            AndroidView(
                modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
                factory = { context ->
                    MyView(context)
                },
                update = { view ->
                    view.selectedItem = index
                },
                onReset = { view ->
                    view.clear()
                }
            )
        }
    }
}

Fragments in Compose

Use the AndroidViewBinding composable to add a Fragment in Compose. AndroidViewBinding has fragment-specific handling such as removing the fragment when the composable leaves the composition.

Do so by inflating an XML containing a FragmentContainerView as the holder for your Fragment.

For example, if you have the my_fragment_layout.xml defined, you could use code like this while replacing the android:name XML attribute with your Fragment's class name:

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

Inflate this fragment in Compose as follows:

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

If you need to use multiple fragments in the same layout, ensure that you have defined a unique ID for each FragmentContainerView.

Calling the Android framework from Compose

Compose operates within the Android framework classes. For example, it's hosted on Android View classes, like Activity or Fragment, and might use Android framework classes like the Context, system resources, Service, or BroadcastReceiver.

To learn more about system resources, see Resources in Compose.

Composition Locals

CompositionLocal classes allow passing data implicitly through composable functions. They're usually provided with a value in a certain node of the UI tree. That value can be used by its composable descendants without declaring the CompositionLocal as a parameter in the composable function.

CompositionLocal is used to propagate values for Android framework types in Compose such as Context, Configuration or the View in which the Compose code is hosted with the corresponding LocalContext, LocalConfiguration, or LocalView. Note that CompositionLocal classes are prefixed with Local for better discoverability with auto-complete in the IDE.

Access the current value of a CompositionLocal by using its current property. For example, the code below shows a toast message by providing LocalContext.current into the Toast.makeToast method.

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

For a more complete example, take a look at the Case Study: BroadcastReceivers section at the end of this document.

Other interactions

If there isn't a utility defined for the interaction you need, the best practice is to follow the general Compose guideline, data flows down, events flow up (discussed at more length in Thinking in Compose). For example, this composable launches a different activity:

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)
    }
}

Case study: Broadcast receivers

For a more realistic example of features you might want to migrate or implement in Compose, and to showcase CompositionLocal and side effects, let's say a BroadcastReceiver needs to be registered from a composable function.

The solution makes use of LocalContext to use the current context, and rememberUpdatedState and DisposableEffect side effects.

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

Next steps

Now that you know the interoperability APIs when using Compose in Views and vice versa, explore the Other considerations page to learn more.