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

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

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

Unfortunately, nested scrolling between the View system and Jetpack Compose is not available yet. You can check progress in this issue tracker bug.

Compose in RecyclerView

Jetpack Compose uses the DisposeOnDetachedFromWindow as the default ViewCompositionStrategy. That means that the Composition is disposed whenever the view is detached from the window.

When using a ComposeView as part of a RecyclerView view holder, the default strategy is inefficient as the underlying Composition instances will remain in memory until the RecyclerView is detached from the window. Disposing the underlying Composition when the ComposeView is no longer needed by the RecyclerView is a good practice.

The disposeComposition function allows you to manually dispose of the underlying Composition of a ComposeView. You can call this function when the view is recycled like so:

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

As covered in the ViewCompositionStrategy for ComposeView section of the Interoperability APIs guide, to make the Compose view holder work in all scenarios, it's necessary to use the DisposeOnViewTreeLifecycleDestroyed strategy.

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

To see ComposeView used in RecyclerView in action, check out the compose_recyclerview branch of the Sunflower app.