Jetpack Compose is designed to work with the established view-based UI approach. If you're building a new app, the best option might be to implement your entire UI with Compose. But if you're modifying an existing app, you might not want to fully migrate your app all at once. Instead, you can combine Compose with your existing UI design implementation.
Adopting Compose in your app
There are two main ways you can integrate Compose with a view-based UI:
You can add Compose elements into your existing UI, either by creating an entirely new Compose-based screen, or by adding Compose elements into an existing activity, fragment or view layout.
You can add a view-based UI element into your composable functions. Doing so lets you add Android views into a Compose-based design.
Migrating the entire app to Compose is best achieved step by step with the granularity the project needs. You can migrate one screen at a time, or even one fragment or any other reusable UI element at a time. You can use several different approaches:
The bottom-up approach starts migrating the smaller UI elements on the screen, like a
Button
or aTextView
, followed by itsViewGroup
elements until everything is converted to composable functions.The top-down approach starts migrating the fragments or view containers, like a
FrameLayout
,ConstraintLayout
, orRecyclerView
, followed by the smaller UI elements on the screen.
These approaches assume each screen is self-contained, but it's also possible to migrate shared UI, like a design system, to Jetpack Compose. See Migrating shared UI below for more information.
Interoperability APIs
While adopting Compose in your app, Compose and view-based UIs could be combined. Here's a list of APIs, recommendations, and tips to make the transition to Compose easier.
Compose in Android Views
You can add Compose-based UI into an existing app that uses a view-based design.
To create a new, entirely Compose-based screen, have your
activity call the setContent()
method, and pass whatever composable functions
you like.
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { // In here, we can call composables!
MaterialTheme {
Greeting(name = "compose")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
This code looks just like what you'd find in a Compose-only app.
If you want to incorporate Compose UI content in a fragment or an existing View
layout, use ComposeView
and call its
setContent()
method. ComposeView
is an Android View
.
You must attach the ComposeView
to a
ViewTreeLifecycleOwner
.
The ViewTreeLifecycleOwner
allows the view to be attached and detached
repeatedly while preserving the composition. ComponentActivity
,
FragmentActivity
and AppCompatActivity
are all examples of classes that
implement ViewTreeLifecycleOwner
.
You can put the ComposeView
in your XML layout just like any other View
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/hello_world"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello Android!" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
In the Kotlin source code, inflate the layout from the layout
resource defined in XML. Then get the
ComposeView
using the XML ID, and call setContent()
to use Compose.
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
return inflater.inflate(
R.layout.fragment_example, container, false
).apply {
findViewById<ComposeView>(R.id.compose_view).setContent {
// In Compose world
MaterialTheme {
Text("Hello Compose!")
}
}
}
}
}
Figure 1. This shows the output of the code that adds Compose elements in a
View UI hierarchy. The "Hello Android!" text is displayed by a
TextView
widget. The "Hello Compose!" text is displayed by a
Compose text element.
You can also include a ComposeView
directly in a fragment if your full screen
is built with Compose, which lets you avoid using an XML layout file entirely.
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
MaterialTheme {
// In Compose world
Text("Hello Compose!")
}
}
}
}
}
If there are multiple ComposeView
elements in the same layout, each one must
have a unique ID for savedInstanceState
to work. There's more information
about this in the SavedInstanceState
section.
class ExampleFragment : Fragment() {
override fun onCreateView(...): View = LinearLayout(...).apply {
addView(ComposeView(...).apply {
id = R.id.compose_view_x
...
})
addView(TextView(...))
addView(ComposeView(...).apply {
id = R.id.compose_view_y
...
})
}
}
}
The ComposeView
IDs are defined in the res/values/ids.xml
file:
<resources>
<item name="compose_view_x" type="id" />
<item name="compose_view_y" type="id" />
</resources>
Android Views in Compose
You can include an Android View hierarchy in a Compose UI. This approach is
particularly useful if you want to use UI elements that are not yet available in
Compose, like
AdView
or
MapView
.
This approach also lets you reuse custom views you may have designed.
To include a view element or hierarchy, use the AndroidView
composable.
AndroidView
is passed a lambda that returns a
View
. AndroidView
also provides an update
callback that is called when the view is inflated. The AndroidView
recomposes
whenever a State
read within the callback changes.
@Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// Adds view to Compose
AndroidView(
modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
factory = { context ->
// Creates custom view
CustomView(context).apply {
// Sets up listeners for View -> Compose communication
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
// View's been inflated or state read in this block has been updated
// Add logic here if necessary
// As selectedItem is read here, AndroidView will recompose
// whenever the state changes
// Example of Compose -> View communication
view.coordinator.selectedItem = selectedItem.value
}
)
}
@Composable
fun ContentExample() {
Column(Modifier.fillMaxSize()) {
Text("Look at this CustomView!")
CustomView()
}
}
To embed an XML layout, use the
AndroidViewBinding
API, which is provided by the androidx.compose.ui:ui-viewbinding
library. To
do this, your project must enable view binding.
AndroidView
, like many other built-in composables, takes a Modifier
parameter that can be used, for example, to set its position in the parent
composable.
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Calling the Android framework from Compose
Compose is tightly bound to the Android framework classes. For example, it's
hosted on Android View classes, like Activity
or Fragment
, and might need to
make use of Android framework classes like the Context
, system resources,
Service
, or BroadcastReceiver
.
To learn more about system resources, check out the Resources in Compose documentation.
Composition Locals
CompositionLocal
classes allow passing data implicitly through composable functions. They're
usually provided with a value in a certain node of the UI tree. That value can
be used by its composable descendants without declaring the CompositionLocal
as a parameter in the composable function.
CompositionLocal
is used to propagate values for Android framework types in
Compose such as Context
, Configuration
or the View
in which the Compose
code is hosted with the corresponding
LocalContext
,
LocalConfiguration
,
or
LocalView
.
Note that CompositionLocal
classes are prefixed with Local
for better
discoverability with auto-complete in the IDE.
Access the current value of a CompositionLocal
by using its current
property. For example, the code below creates a custom view using the Context
available in that part of the Compose UI tree by calling LocalContext.current
.
@Composable
fun rememberCustomView(): CustomView {
val context = LocalContext.current
return remember { CustomView(context).apply { /*...*/ } }
}
For a more complete example, take a look at the Case Study: BroadcastReceivers section at the end of this document.
Other interactions
If there isn't a utility defined for the interaction you need, the best practice is to follow the general Compose guideline, data flows down, events flow up (discussed at more length in Thinking in Compose). For example, this composable launches a different activity:
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// get data from savedInstanceState
setContent {
MaterialTheme {
ExampleComposable(data, onButtonClick = {
startActivity(/*...*/)
})
}
}
}
}
@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
Button(onClick = onButtonClick) {
Text(data.title)
}
}
Integration with common libraries
To see how Compose is integrated with common libraries like
ViewModel
, Flow
,
Paging
, or
Hilt
, check out the Compose
integration with common libraries guide.
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( /* ... */)
}
}
}
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 */
}
}
}
Architecture and state source of truth
Unidirectional Data Flow (UDF) architecture patterns work seamlessly with Compose. If the app uses other types of architecture patterns instead, like Model View Presenter (MVP), we recommend you migrate that part of the UI to UDF before or whilst adopting Compose.
ViewModels in Compose
If you use the Architecture Components
ViewModel library, you can access a
ViewModel
from any composable by
calling the
viewModel()
function, as explained in the Compose integration with common libraries
documentation.
When adopting Compose, be careful about using the same ViewModel
type in
different composables as ViewModel
elements follow View-lifecycle scopes. The
scope will be either the host activity, fragment or the navigation graph if the
Navigation library is used.
For example, if the composables are hosted in an activity, viewModel()
always
returns the same instance that will only be cleared when the activity finishes.
In the following example, the same user will be greeted twice because the same
GreetingViewModel
instance is reused in all composables under the host
activity. The first ViewModel
instance created is reused in other composables.
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Column {
Greeting("user1")
Greeting("user2")
}
}
}
}
}
@Composable
fun Greeting(userId: String) {
val greetingViewModel: GreetingViewModel = viewModel(
factory = GreetingViewModelFactory(userId)
)
val messageUser by greetingViewModel.message.observeAsState("")
Text(messageUser)
}
class GreetingViewModel(private val userId: String) : ViewModel() {
private val _message = MutableLiveData("Hi $userId")
val message: LiveData<String> = _message
}
As navigation graphs also scope ViewModel
elements, composables that are a
destination in a navigation graph have a different instance of the ViewModel
.
In this case, the ViewModel
is scoped to the lifecycle of the destination and
it will be cleared when the destination is removed from the backstack. In the
following example, when the user navigates to the Profile screen, a new
instance of GreetingViewModel
is created.
@Composable
fun MyScreen() {
NavHost(rememberNavController(), startDestination = "profile/{userId}") {
/* ... */
composable("profile/{userId}") { backStackEntry ->
Greeting(backStackEntry.arguments?.getString("userId") ?: "")
}
}
}
@Composable
fun Greeting(userId: String) {
val greetingViewModel: GreetingViewModel = viewModel(
factory = GreetingViewModelFactory(userId)
)
val messageUser by greetingViewModel.message.observeAsState("")
Text(messageUser)
}
State source of truth
When you adopt Compose in one part of the UI, it's possible that Compose and the View system code will need to share data. When possible, we recommend you encapsulate that shared state in another class that follows UDF best practices used by both platforms, for example, in a ViewModel that exposes a stream of the shared data to emit data updates.
However, that's not always possible if the data to be shared is mutable or is tightly bound to a UI element. In that case, one system must be the source of truth, and that system needs to share any data updates to the other system. As a general rule of thumb, the source of truth should be owned by whichever element is closer to the root of the UI hierarchy.
Compose as the source of truth
Use the
SideEffect
composable to publish Compose state to non-compose code. In this case, the
source of truth is kept in a composable which sends state updates.
As example, an
OnBackPressedCallback
needs to be registered to listen for the back button being pressed on a
OnBackPressedDispatcher
.
To communicate whether the callback should be enabled or not, use SideEffect
to update its value.
@Composable
fun BackHandler(
enabled: Boolean,
backDispatcher: OnBackPressedDispatcher,
onBack: () -> Unit
) {
// Safely update the current `onBack` lambda when a new one is provided
val currentOnBack by rememberUpdatedState(onBack)
// Remember in Composition a back callback that calls the `onBack` lambda
val backCallback = remember {
// Always intercept back events. See the SideEffect for a more complete version
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
currentOnBack()
}
}
}
// On every successful composition, update the callback with the `enabled` value
// to tell `backCallback` whether back events should be intercepted or not
SideEffect {
backCallback.isEnabled = enabled
}
// If `backDispatcher` changes, dispose and reset the effect
DisposableEffect(backDispatcher) {
// Add callback to the backDispatcher
backDispatcher.addCallback(backCallback)
// When the effect leaves the Composition, remove the callback
onDispose {
backCallback.remove()
}
}
}
For more information about side effects, check out the Lifecycle and Side effects documentation.
View system as the source of truth
If the View system owns the state and shares it with Compose, we recommend that
you wrap the state in mutableStateOf
objects to make it thread-safe for
Compose. If you use this approach, composable functions are simplified because
they no longer have the source of truth, but the View system needs to update the
mutable state and the Views that use that state.
In the following example, a CustomViewGroup
contains a TextView
and a
ComposeView
with a TextField
composable inside. The TextView
needs to show
the content of what the user types in the TextField
.
class CustomViewGroup @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {
// Source of truth in the View system as mutableStateOf
// to make it thread-safe for Compose
private var text by mutableStateOf("")
private val textView: TextView
init {
orientation = VERTICAL
textView = TextView(context)
val composeView = ComposeView(context).apply {
setContent {
MaterialTheme {
TextField(value = text, onValueChange = { updateState(it) })
}
}
}
addView(textView)
addView(composeView)
}
// Update both the source of truth and the TextView
private fun updateState(newValue: String) {
text = newValue
textView.text = newValue
}
}
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, check out the state source of truth section above.
Testing
You can test your combined View and Compose code together by using the
createAndroidComposeRule()
API. For more information, see Testing your Compose layout.
Case Study: BroadcastReceivers
For a more realistic example of features you might want to migrate or implement
in Compose and to showcase CompositionLocal
and side
effects, let's say a
BroadcastReceiver
needs to be registered from
a composable function.
The solution makes use of LocalContext
to use the current context, and
rememberUpdatedState
and DisposableEffect
side effects.
@Composable
fun SystemBroadcastReceiver(
systemAction: String,
onSystemEvent: (intent: Intent?) -> Unit
) {
// Grab the current context in this part of the UI tree
val context = LocalContext.current
// Safely use the latest onSystemEvent lambda passed to the function
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
// If either context or systemAction changes, unregister and register again
DisposableEffect(context, systemAction) {
val intentFilter = IntentFilter(systemAction)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onSystemEvent(intent)
}
}
context.registerReceiver(broadcast, intentFilter)
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
}
@Composable
fun HomeScreen() {
SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
val isCharging = /* Get from batteryStatus ... */ true
/* Do something if the device is charging */
}
/* Rest of the HomeScreen */
}