Integrating Compose with your existing UI

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.

Theming

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 for more information.

Multiple sources of truth

An existing app is likely to have a large amount of theming and styling for Views. When you introduce Compose in an existing app, you need to migrate the theme(s) to use MaterialTheme for any Compose screens. This means your app's theming will have two sources of truth: the View-based theme and the Compose theme. Any changes to your styling would need to be made in multiple places.

If your plan is to fully migrate the app to Compose, you'll eventually need to create a Compose version of the existing theme. The issue is, the earlier in your development process you create your Compose theme, the more maintenance you'll have to do during development.

Material Theme Adapter

If you're using a Theme.MaterialComponents.* theme from the MDC-Android library in your app, the Material Theme Adapter library allows you to easily re-use the Material 2 color, typography, and shape theming from your existing View-based XML theme in your composables.

Use the MdcTheme composable:

import com.google.accompanist.themeadapter.material.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {
            // Use MdcTheme instead of M2 MaterialTheme
            // Colors, typography, and shapes have been read from the
            // View-based theme used in this Activity
            MdcTheme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

See the Material Theme Adapter library documentation for more information.

Material 3 Theme Adapter

If you're using a Theme.Material3.* theme from the MDC-Android library in your app, the Material 3 Theme Adapter library allows you to easily re-use the Material 3 color, typography, and shape theming from your existing View-based XML theme in your composables.

Use the Mdc3Theme composable:

import com.google.accompanist.themeadapter.material3.Mdc3Theme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {
            // Use Mdc3Theme instead of M3 MaterialTheme
            // Color scheme, typography, and shapes have been read from the
            // View-based theme used in this Activity
            Mdc3Theme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

See the Material 3 Theme Adapter library documentation for more information.

AppCompat Theme Adapter

The AppCompat Theme Adapter library allows you to easily reuse AppCompat XML themes for theming in Jetpack Compose. It creates an M2 MaterialTheme with the color and typography values from the context's theme.

Use the AppCompatTheme composable:

import com.google.accompanist.themeadapter.appcompat.AppCompatTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppCompatTheme {
                // Colors and typography have been read from the
                // View-based theme used in this Activity
                // Shapes are the default for M2 as this didn't exist in M1
                ExampleComposable(/*...*/)
            }
        }
    }
}

See the AppCompat Theme Adapter library documentation for more information.

Custom theme attributes

The Core Theme Adapter library is used by all of the above Accompanist theme adapter libraries and contains common logic for various XML resource to Compose conversions. These resource utilities can be used to parse custom theme attributes.

See the Core Theme Adapter library documentation for more information.

Default component styles

The Accompanist theme adapter libraries don't read any theme-defined default widget styles. This is because Compose doesn't have the concept of default composables.

See Custom design systems in Compose for more information.

Theme overlays

When migrating View-based screens to Compose, watch out for usages of the android:theme attribute. It's likely you need a new MaterialTheme in that part of the Compose UI tree.

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

See the accompanists-insets library documentation for more information.

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. Using tools like BoxWithConstraints, decisions can be made based on the space available to individual elements, which isn’t possible with qualified resources:

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

Read 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.