Integrating Compose with your existing UI

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

If you have an app with a View-based UI, you may not want to rewrite its entire UI all at once. This page will help you add new Compose elements into your existing UI.

Migrating shared UI

If you are migrating gradually to Compose, you might need to use shared UI elements in both Compose and the View system. For example, if your app has a custom CallToActionButton component, you might need to use it in both Compose and View-based screens.

In Compose, shared UI elements become composables that can be reused across the app regardless of the element being styled using XML or being a custom view. For example, you'd create a CallToActionButton composable for your custom call to action Button component.

In order to use the composable in View-based screens, you need to create a custom view wrapper that extends from AbstractComposeView. In its overridden Content composable, place the composable you created wrapped in your Compose theme as shown in the example below:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Notice that the composable parameters become mutable variables inside the custom view. This makes the custom CallToActionViewButton view inflatable and usable, with for example View Binding, like a traditional view. See the example below:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

If the custom component contains mutable state, see State source of truth.

Migrating your app's theme

Material Design is the recommended design system for theming Android apps.

For View-based apps there are three versions of Material available:

  • Material Design 1 using the AppCompat library (i.e. Theme.AppCompat.*)
  • Material Design 2 using the MDC-Android library (i.e. Theme.MaterialComponents.*)
  • Material Design 3 using the MDC-Android library (i.e. Theme.Material3.*)

For Compose apps there are two versions of Material available:

  • Material Design 2 using the Compose Material library (i.e. androidx.compose.material.MaterialTheme)
  • Material Design 3 using the Compose Material 3 library (i.e. androidx.compose.material3.MaterialTheme)

We recommend using the latest version — Material 3 — if your app's design system is in a position to do so. There are migration guides available for both Views and Compose:

When creating new screens in Compose, regardless of which version of Material Design you're using, ensure that you apply a MaterialTheme before any composables that emit UI from the Compose Material libraries. The Material components (Button, Text, etc.) depend on a MaterialTheme being in place and their behaviour is undefined without it.

All Jetpack Compose samples use a custom Compose theme built on top of MaterialTheme.

See Design systems in Compose and Migrating XML themes to Compose to learn more.

WindowInsets and IME Animations

Since Compose 1.2.0, you can handle WindowInsets by using modifiers to handle them within your layouts. IME animations are also supported.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
              MyScreen()
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Animation showing a UI element scrolling up and down to make way for a keyboard

Figure 2. IME animations

Prioritize splitting state from presentation

Traditionally, a View is stateful. A View manages fields that describe what to display, in addition to how to display it. When you convert a View to Compose, look to separate the data being rendered to achieve a unidirectional data flow, as explained further in state hoisting.

For example, a View has a visibility property that describes if it is visible, invisible, or gone. This is an inherent property of the View. While other pieces of code may change the visibility of a View, only the View itself really knows what its current visibility is. The logic for ensuring that a View is visible can be error prone, and is often tied to the View itself.

By contrast, Compose makes it easy to display entirely different composables using conditional logic in Kotlin:

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

By design, CautionIcon doesn’t need to know or care why it is being displayed, and there is no concept of visibility: it either is in the Composition, or it isn’t.

By cleanly separating state management and presentation logic, you can more freely change how you display content as a conversion of state to UI. Being able to hoist state when needed also makes Composables more reusable, since state ownership is more flexible.

Promote encapsulated and reusable components

View elements often have some idea of where they live: inside an Activity, a Dialog, a Fragment or somewhere inside another View hierarchy. Because they are often inflated from static layout files, the overall structure of a View tends to be very rigid. This results in tighter coupling, and makes it harder for a View to be changed or reused.

For example, a custom View might assume that it has a child view of a certain type with a certain id, and change its properties directly in response to some action. This tightly couples those View elements together: The custom View may crash or be broken if it can’t find the child, and the child likely can’t be reused without the custom View parent.

This is less of a problem in Compose with reusable composables. Parents can easily specify state and callbacks, so reusable Composables can be written without having to know the exact place where they will be used.

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

In the example above, all three parts are more encapsulated and less coupled:

  • ImageWithEnabledOverlay only needs to know what the current isEnabled state is. It doesn’t need to know that ControlPanelWithToggle exists, or even how it is controllable.

  • ControlPanelWithToggle doesn’t know that ImageWithEnabledOverlay exists. There could be zero, one, or more ways that isEnabled is displayed, and ControlPanelWithToggle wouldn’t have to change.

  • To the parent, it doesn’t matter how deeply nested ImageWithEnabledOverlay or ControlPanelWithToggle are. Those children could be animating changes, swapping out content, or passing content on to other children.

This pattern is known as the inversion of control, which you can read more about in the CompositionLocal documentation.

Handling screen size changes

Having different resources for different window sizes is one of the main ways to create responsive View layouts. While qualified resources are still an option for screen-level layout decisions, Compose makes it much easier to change layouts entirely in code with normal conditional logic. See support different screen sizes to learn more.

Additionally, refer to build adaptive layouts to learn about the techniques Compose offers to build adaptive UIs.

Nested scrolling with Views

For more information on how to enable nested scrolling interop between scrollable View elements and scrollable composables, nested in both directions, read through Nested scrolling interop.

Compose in RecyclerView

Composables in RecyclerView are performant since RecyclerView version 1.3.0-alpha02. Make sure you on at least version 1.3.0-alpha02 of RecyclerView to see those benefits.