Interoperability APIs

While adopting Compose in your app, Compose and view-based UIs could be combined. Here's a list of APIs, recommendations, and tips to make the transition to Compose easier.

Compose in Android Views

You can add Compose-based UI into an existing app that uses a view-based design.

To create a new, entirely Compose-based screen, have your activity call the setContent() method, and pass whatever composable functions you like.

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

This code looks just like what you'd find in a Compose-only app.

If you want to incorporate Compose UI content in a fragment or an existing View layout, use ComposeView and call its setContent() method. ComposeView is an Android View. You must attach the ComposeView to a ViewTreeLifecycleOwner. The ViewTreeLifecycleOwner allows the view to be attached and detached repeatedly while preserving the composition. ComponentActivity, FragmentActivity and AppCompatActivity are all examples of classes that implement ViewTreeLifecycleOwner.

You can put the ComposeView in your XML layout just like any other 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>

In the Kotlin source code, inflate the layout from the layout resource defined in XML. Then get the ComposeView using the XML ID, and call setContent() to use 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!")
                }
            }
        }
    }
}

Two slightly different text elements, one above the other

Figure 1. This shows the output of the code that adds Compose elements in a View UI hierarchy. The "Hello Android!" text is displayed by a TextView widget. The "Hello Compose!" text is displayed by a Compose text element.

You can also include a ComposeView directly in a fragment if your full screen is built with Compose, which lets you avoid using an XML layout file entirely.

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

If there are multiple ComposeView elements in the same layout, each one must have a unique ID for savedInstanceState to work.

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

The ComposeView IDs are defined in the res/values/ids.xml file:

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

Android 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 or MapView. 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.

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

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.

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 AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

Calling the Android framework from Compose

Compose is tightly bound to the Android framework classes. For example, it's hosted on Android View classes, like Activity or Fragment, and might need to make use of Android framework classes like the Context, system resources, Service, or BroadcastReceiver.

To learn more about system resources, check out the Resources in Compose documentation.

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 creates a custom view using the Context available in that part of the Compose UI tree by calling LocalContext.current.

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

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

Case Study: BroadcastReceivers

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?) {
                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 */
}