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 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 : ComponentActivity() {
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.
ViewCompositionStrategy for ComposeView
By default, Compose disposes of the Composition
whenever the view becomes detached from a window. Compose UI View
types such as ComposeView
and AbstractComposeView
use a ViewCompositionStrategy
that defines this behavior.
By default, Compose uses the
DisposeOnDetachedFromWindow
strategy. However, this default value might be undesirable in some
situations when the Compose UI View
types are used in:
Fragments. The Composition must follow the fragment's view lifecycle for Compose UI
View
types to save state.Transitions. Anytime the Compose UI
View
is used as part of a transition, it will be detached from its window when the transition starts instead of when the transition ends, thus causing your composable to dispose of its state while it is still on screen.RecyclerView
view holders, or your own lifecycle-managed customView
.
In some of these situations, the app can also slowly leak memory from
Composition instances unless you manually call
AbstractComposeView.disposeComposition
.
For disposing Compositions automatically when they're no longer needed, set a
different strategy or create your own by calling the
setViewCompositionStrategy
method. For example, the
DisposeOnLifecycleDestroyed
strategy disposes the Composition when the lifecycle
is destroyed. This
strategy is appropriate for Compose UI View
types that share a 1 to 1
relationship with a known LifecycleOwner
.
When the LifecycleOwner
is not known, the
DisposeOnViewTreeLifecycleDestroyed
can be used.
See this API in action in the ComposeView in Fragments section.
ComposeView in Fragments
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 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, set a Composition strategy that works best for
the host View
, and call setContent()
to use Compose.
class ExampleFragment : Fragment() {
private var _binding: FragmentExampleBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentExampleBinding.inflate(inflater, container, false)
val view = binding.root
binding.composeView.apply {
// Dispose of the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
// In Compose world
MaterialTheme {
Text("Hello Compose!")
}
}
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
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 {
// Dispose of the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
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.
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>
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
.
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. 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 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.
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Fragments in Compose
Use the AndroidViewBinding
composable to add a Fragment
in Compose.
Do so by inflating an XML containing a FragmentContainerView
as the holder for your Fragment
.
For example, if you have the my_fragment_layout.xml
defined, you could use
code like this while replacing the android:name
XML attribute with your
Fragment
's class name:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
Inflate this fragment in Compose as follows:
@Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
If you need to use multiple fragments in the same layout, ensure that you have
defined a unique ID for each FragmentContainerView
.
Calling the Android framework from Compose
Compose operates within 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 : ComponentActivity() {
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)
}
}
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?) {
currentOnSystemEvent(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 */
}