Google is committed to advancing racial equity for Black communities. See how.

Compose interoperability

Jetpack Compose is designed to work with the established view-based UI approach. If you're building a new app, the best option might be to implement your entire UI with Compose. But if you're modifying an existing app, you might not want to migrate your app. Instead, you can combine Compose with your existing UI design.

There are two main ways you can combine Compose with a view-based UI:

  • You can add Compose elements into your existing UI, either by creating an entirely new Compose-based screen, or by adding Compose elements into an existing fragment or view layout.
  • You can add a view-based UI element into your composable functions. Doing so lets you add non-Compose widgets into a Compose-based design.

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. There's more information about this in the SavedInstanceState section.

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

    val context = AmbientContext.current
    val customView = remember {
        // Creates custom view
        CustomView(context).apply {
            // Sets up listeners for View -> Compose communication
            myView.setOnClickListener {
                selectedItem.value = 1
            }
        }
    }

    // Adds view to Compose
    AndroidView({ customView }) { view ->
        // View's been inflated - 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.

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

Calling the View system from Compose

The Compose framework offers a number of APIs to let your Compose code interact with a view-based UI.

System resources

The Compose framework offers ...Resource() helper methods to let your Compose code get resources from a view-based UI hierarchy. Here are some examples:

Text(
    text = stringResource(R.string.ok),
    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)

Icon(
    imageVector = vectorResource(R.drawable.ic_plane),
    tint = colorResource(R.color.Blue700)
)

Context

The ContextAmbient.current property gives you the current context. For example, this code creates a view in the current context:

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

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

Integration with common libraries

You can use your favorite libraries in Compose. This section describes how to incorporate a few of the most useful libraries.

ViewModel

If you use the Architecture Components ViewModel library, you can access a ViewModel from any composable by calling the viewModel() function.

class ExampleViewModel() : ViewModel() { /*...*/ }

@Composable
fun MyExample() {
    val viewModel: ExampleViewModel = viewModel()

    // use viewModel here
}

viewModel() returns an existing ViewModel or creates a new one in the given scope. The ViewModel is retained as long as the scope is alive. For example, if the composable is used in an activity, viewModel() returns the same instance until the activity is finished or the process is killed.

@Composable
fun MyExample() {
    // Returns the same instance as long as the activity is alive,
    // just as if you grabbed the instance from an Activity or Fragment
    val viewModel: ExampleViewModel = viewModel()
}

@Composable
fun MyExample2() {
    val viewModel: ExampleViewModel = viewModel() // Same instance as in MyExample
}

If your ViewModel has dependencies, viewModel() takes an optional ViewModelProvider.Factory as a parameter.

Streams of data

Compose comes with extensions for Android's most popular stream-based solutions. Each of these extensions is provided by a different artifact:

These artifacts register as a listener and represent the values as a State. Whenever a new value is emitted, Compose recomposes those parts of the UI where that state.value is used. For example, in this code, ShowData recomposes every time exampleLiveData emits a new value.

@Composable
fun MyExample() {
    val viewModel: ExampleViewModel = viewModel()
    val dataExample = viewModel.exampleLiveData.observeAsState()

    // Because the state is read here,
    // MyExample recomposes whenever dataExample changes.
    dataExample.value?.let {
        ShowData(dataExample)
    }
}

Asynchronous operations in Compose

Compose provides mechanisms to let you execute asynchronous operations from within your composables.

For callback-based APIs, you can use a combination of a MutableState and onCommit(). Use MutableState to store a callback's result and recompose the affected UI when the result changes. Use onCommit() to execute an operation whenever a parameter changes. You can also define an onDispose() method to clear any pending operations if the composition ends before the operation has finished. The following example shows how these APIs work together.

@Composable
fun fetchImage(url: String): ImageBitmap? {
    // Holds our current image, and will be updated by the onCommit lambda below
    var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }

    onCommit(url) {
        // This onCommit lambda will be invoked every time url changes

        val listener = object : ExampleImageLoader.Listener() {
            override fun onSuccess(bitmap: Bitmap) {
                // When the image successfully loads, update our image state
                image = bitmap.asImageBitmap()
            }
        }

        // Now execute the image loader
        val imageLoader = ExampleImageLoader.get()
        imageLoader.load(url).into(listener)

        onDispose {
            // If we leave composition, cancel any pending requests
            imageLoader.cancel(listener)
        }
    }

    // Return the state-backed image property. Any callers of this function
    // will be recomposed once the image finishes loading
    return image
}

If the asynchronous operation is a suspending function, you can use launchInComposition() instead:

/** Example suspending loadImage function */
suspend fun loadImage(url: String): ImageBitmap = TODO()

@Composable
fun fetchImage(url: String): ImageBitmap? {
    // This holds our current image, and will be updated by the
    // launchInComposition lambda below
    var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }

    // LaunchedEffect will automatically launch a coroutine to execute
    // the given block. If the `url` changes, any previously launched coroutine
    // will be cancelled, and a new coroutine launched.
    LaunchedEffect(url) {
        image = loadImage(url)
    }

    // Return the state-backed image property
    return image
}

SavedInstanceState

Use savedInstanceState to restore your UI state after an activity or process is recreated. savedInstanceState retains state across recompositions. In addition, savedInstanceState also retains state across activity and process recreation.

@Composable
fun MyExample() {
    var selectedId by savedInstanceState<String?> { null }
    /*...*/
}

All data types that are added to the Bundle are saved automatically. If you want to save something that cannot be added to the Bundle, there are several options.

The simplest solution is to add the @Parcelize annotation to the object. The object becomes parcelable, and can be bundled. For example, this code makes a parcelable City data type and saves it to the state.

@Parcelize
data class City(val name: String, val country: String)

@Composable
fun MyExample() {
  var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}

If for some reason @Parcelize is not suitable, you can use mapSaver to define your own rule for converting an object into a set of values that the system can save to the Bundle.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, nameKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun MyExample() {
    var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
}

To avoid needing to define the keys for the map, you can also use listSaver and use its indices as keys:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun MyExample() {
    var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
    /*...*/
}

Theming

If you’re using Material Design Components for Android in your app, the MDC Compose Theme Adapter library allows you to easily re-use the color, typography and shape theming from your existing themes, from within your composeables:

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // We use MdcTheme instead of MaterialTheme {}
            MdcTheme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

Testing

You can test your combined View and Compose code together by using the createAndroidComposeRule() API. For more information, see Testing your Compose layout.

Learn more

To learn more about integrating Jetpack Compose with your existing UI, try the Migrating to Jetpack Compose codelab.