Interoperability APIs

Stay organized with collections Save and categorize content based on your preferences.

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

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

ViewCompositionStrategy for ComposeView

By default, Compose disposes of the Composition whenever the view becomes detached from a window. Compose UI View types such as ComposeView and AbstractComposeView use a ViewCompositionStrategy that defines this behavior.

By default, Compose uses the DisposeOnDetachedFromWindowOrReleasedFromPool strategy. However, this default value might be undesirable in some situations when the Compose UI View types are used in:

  • Fragments. The Composition must follow the fragment's view lifecycle for Compose UI View types to save state.

  • Transitions. Anytime the Compose UI View is used as part of a transition, it will be detached from its window when the transition starts instead of when the transition ends, thus causing your composable to dispose of its state while it is still on screen.

  • Your own lifecycle-managed custom View.

In some of these situations, the app can also slowly leak memory from Composition instances unless you manually call AbstractComposeView.disposeComposition.

For disposing Compositions automatically when they're no longer needed, set a different strategy or create your own by calling the setViewCompositionStrategy method. For example, the DisposeOnLifecycleDestroyed strategy disposes the Composition when the lifecycle is destroyed. This strategy is appropriate for Compose UI View types that share a 1 to 1 relationship with a known LifecycleOwner. When the LifecycleOwner is not known, the DisposeOnViewTreeLifecycleDestroyed can be used.

See this API in action in ComposeView in Fragments.

ComposeView in Fragments

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 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, set a Composition strategy that works best for the host View, and call setContent() to use 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
  }
}

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

Multiple ComposeViews in the same layout

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

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

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>

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

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

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_height="match_parent"
  android:layout_width="match_parent"
  android:name="com.example.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 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 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: 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?) {
        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 */
}