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:
- Material 1 to Material 2 in Views
- Material 2 to Material 3 in Views
- Material 2 to Material 3 in 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( /* ... */)
}
}
}
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 currentisEnabled
state is. It doesn’t need to know thatControlPanelWithToggle
exists, or even how it is controllable.ControlPanelWithToggle
doesn’t know thatImageWithEnabledOverlay
exists. There could be zero, one, or more ways thatisEnabled
is displayed, andControlPanelWithToggle
wouldn’t have to change.To the parent, it doesn’t matter how deeply nested
ImageWithEnabledOverlay
orControlPanelWithToggle
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.