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(
            backgroundColor = MaterialTheme.colors.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<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @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 ExampleActivity : Activity() {

    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.something)
            onClick = { /* Do something */ }
        }
    }
}

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

Theming

Following Material Design, using the Material Design Components for Android (MDC) library, is the recommended way to theme Android apps. As covered in the Compose Theming documentation, Compose implements these concepts with the MaterialTheme composable.

When creating new screens in Compose, you should ensure that you apply a MaterialTheme before any composables that emit UI from the material components library. 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.

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'll need to migrate the theme to use MaterialTheme for any Compose screens. This means your app's theming will have 2 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.

MDC Compose Theme adapter

If you're using the MDC library in your Android app, the MDC Compose Theme Adapter library allows you to easily re-use the color, typography and shape theming from your existing View-based themes, in your composables:

import com.google.android.material.composethemeadapter.MdcTheme

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

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

See the MDC library documentation for more information.

AppCompat Compose Theme adapter

AppCompat Compose Theme Adapter library allows you to easily re-use AppCompat XML themes for theming in Jetpack Compose. It creates a MaterialTheme with the color and typography values from the context's theme.

import com.google.accompanist.appcompattheme.AppCompatTheme

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

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Default component styles

Both the MDC and AppCompat Compose Theme Adapter libraries don't read any theme-defined default widget styles. This is because Compose doesn't have the concept of default composables.

Read more about component styles and custom design systems in the Theming documentation.

Theme Overlays in Compose

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.

Read more about this in the Theming guide.

WindowInsets and IME Animations

You can handle WindowInsets by using the accompanist-insets library, which provides composables and modifiers to handle them within your layouts, as well as support for IME animations.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

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

Figure 2. IME animations using the accompanist-insets library.

See the accompanists-insets library documentation for more information.

Handling screen size changes

When migrating an app that uses different XML layouts depending on the screen size, use the BoxWithConstraints composable to know the minimum and maximum size a composable can occupy.

@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 */
        }
    }
}