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 migrate your app. Instead, you can combine Compose with your existing UI design.
There are two main ways you can combine 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 fragment or view layout.
- You can add a view-based UI element into your composable functions. Doing so lets you add non-Compose widgets into a Compose-based design.
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) }
val context = AmbientContext.current
val customView = remember {
// Creates custom view
CustomView(context).apply {
// Sets up listeners for View -> Compose communication
myView.setOnClickListener {
selectedItem.value = 1
}
}
}
// Adds view to Compose
AndroidView({ customView }) { view ->
// View's been inflated - 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.
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Calling the View system from Compose
The Compose framework offers a number of APIs to let your Compose code interact with a view-based UI.
System resources
The Compose framework offers ...Resource()
helper methods to let your Compose
code get resources from a view-based UI hierarchy. Here are some examples:
Text(
text = stringResource(R.string.ok),
modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)
Icon(
imageVector = vectorResource(R.drawable.ic_plane),
tint = colorResource(R.color.Blue700)
)
Context
The
ContextAmbient
.current
property gives you the current context. For example, this code creates a view in
the current context:
@Composable
fun rememberCustomView(): CustomView {
val context = AmbientContext.current
return remember { CustomView(context).apply { /*...*/ } }
}
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
You can use your favorite libraries in Compose. This section describes how to incorporate a few of the most useful libraries.
ViewModel
If you use the Architecture Components
ViewModel library, you can access a
ViewModel
from any composable by
calling the
viewModel()
function.
class ExampleViewModel() : ViewModel() { /*...*/ }
@Composable
fun MyExample() {
val viewModel: ExampleViewModel = viewModel()
// use viewModel here
}
viewModel()
returns an existing ViewModel
or creates a new one in the given
scope. The ViewModel
is retained as long as the scope is alive. For example,
if the composable is used in an activity, viewModel()
returns the same
instance until the activity is finished or the process is killed.
@Composable
fun MyExample() {
// Returns the same instance as long as the activity is alive,
// just as if you grabbed the instance from an Activity or Fragment
val viewModel: ExampleViewModel = viewModel()
}
@Composable
fun MyExample2() {
val viewModel: ExampleViewModel = viewModel() // Same instance as in MyExample
}
If your ViewModel has dependencies, viewModel()
takes an optional
ViewModelProvider.Factory
as a parameter.
Streams of data
Compose comes with extensions for Android's most popular stream-based solutions. Each of these extensions is provided by a different artifact:
LiveData.observeAsState()
included in theandroidx.compose.runtime:runtime-livedata:$composeVersion
artifact.Flow.collectAsState()
doesn't require extra dependencies.Observable.subscribeAsState()
included in theandroidx.compose.runtime:runtime-rxjava2:$composeVersion
orandroidx.compose.runtime:runtime-rxjava3:$composeVersion
artifact.
These artifacts register as a listener and represent the values as a
State
. Whenever a new value
is emitted, Compose recomposes those parts of the UI where that state.value
is
used. For example, in this code, ShowData
recomposes every time
exampleLiveData
emits a new value.
@Composable
fun MyExample() {
val viewModel: ExampleViewModel = viewModel()
val dataExample = viewModel.exampleLiveData.observeAsState()
// Because the state is read here,
// MyExample recomposes whenever dataExample changes.
dataExample.value?.let {
ShowData(dataExample)
}
}
Asynchronous operations in Compose
Compose provides mechanisms to let you execute asynchronous operations from within your composables.
For callback-based APIs, you can use a combination of a
MutableState
and
onCommit()
. Use
MutableState
to store a callback's result and recompose the affected UI when
the result changes. Use onCommit()
to execute an operation whenever a
parameter changes. You can also define an
onDispose()
method to clear any pending operations if the composition ends before the
operation has finished. The following example shows how these APIs work
together.
@Composable
fun fetchImage(url: String): ImageBitmap? {
// Holds our current image, and will be updated by the onCommit lambda below
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
onCommit(url) {
// This onCommit lambda will be invoked every time url changes
val listener = object : ExampleImageLoader.Listener() {
override fun onSuccess(bitmap: Bitmap) {
// When the image successfully loads, update our image state
image = bitmap.asImageBitmap()
}
}
// Now execute the image loader
val imageLoader = ExampleImageLoader.get()
imageLoader.load(url).into(listener)
onDispose {
// If we leave composition, cancel any pending requests
imageLoader.cancel(listener)
}
}
// Return the state-backed image property. Any callers of this function
// will be recomposed once the image finishes loading
return image
}
If the asynchronous operation is a suspending function, you can use
launchInComposition()
instead:
/** Example suspending loadImage function */
suspend fun loadImage(url: String): ImageBitmap = TODO()
@Composable
fun fetchImage(url: String): ImageBitmap? {
// This holds our current image, and will be updated by the
// launchInComposition lambda below
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
// LaunchedEffect will automatically launch a coroutine to execute
// the given block. If the `url` changes, any previously launched coroutine
// will be cancelled, and a new coroutine launched.
LaunchedEffect(url) {
image = loadImage(url)
}
// Return the state-backed image property
return image
}
SavedInstanceState
Use savedInstanceState
to restore your UI state after an activity or process
is recreated. savedInstanceState
retains state across recompositions.
In addition, savedInstanceState
also retains state
across activity and process recreation.
@Composable
fun MyExample() {
var selectedId by savedInstanceState<String?> { null }
/*...*/
}
All data types that are added to the Bundle
are saved automatically. If you
want to save something that cannot be added to the Bundle
, there are several
options.
The simplest solution is to add the
@Parcelize
annotation to the object. The object becomes parcelable, and can be bundled. For
example, this code makes a parcelable City
data type and saves it to the
state.
@Parcelize
data class City(val name: String, val country: String)
@Composable
fun MyExample() {
var selectedCity = savedInstanceState { City("Madrid", "Spain") }
}
If for some reason @Parcelize
is not suitable, you can use mapSaver
to
define your own rule for converting an object into a set of values that the
system can save to the Bundle
.
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, nameKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun MyExample() {
var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
}
To avoid needing to define the keys for the map, you can also use listSaver
and use its indices as keys:
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun MyExample() {
var selectedCity = savedInstanceState(CitySaver) { City("Madrid", "Spain") }
/*...*/
}
Theming
If you’re using Material Design Components for Android in your app, the MDC Compose Theme Adapter library allows you to easily re-use the color, typography and shape theming from your existing themes, from within your composeables:
class ExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// We use MdcTheme instead of MaterialTheme {}
MdcTheme {
ExampleComposable(/*...*/)
}
}
}
}
Testing
You can test your combined View and Compose code together by using the
createAndroidComposeRule()
API. For more information, see Testing your Compose layout.
Learn more
To learn more about integrating Jetpack Compose with your existing UI, try the Migrating to Jetpack Compose codelab.