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
DisposeOnDetachedFromWindowOrReleasedFromPool
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.Your own lifecycle-managed custom
View
.
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 ComposeView in Fragments.
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(ViewCompositionStrategy.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 ExampleFragmentNoXml : 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(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { MaterialTheme { // In Compose world Text("Hello Compose!") } } } } }
Multiple ComposeViews in the same layout
If there are multiple ComposeView
elements in the same layout, each one must
have a unique ID for savedInstanceState
to work.
class ExampleFragmentMultipleComposeView : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = LinearLayout(requireContext()).apply { addView( ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) id = R.id.compose_view_x // ... } ) addView(TextView(requireContext())) addView( ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) 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() { var selectedItem by remember { mutableStateOf(0) } // Adds view to Compose AndroidView( modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree factory = { context -> // Creates view MyView(context).apply { // Sets up listeners for View -> Compose communication setOnClickListener { selectedItem = 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.selectedItem = selectedItem } ) } @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.
AndroidViewBinding
has fragment-specific handling such as removing the
fragment when the composable leaves the composition.
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 shows a toast message by providing
LocalContext.current
into the Toast.makeToast
method.
@Composable fun ToastGreetingButton(greeting: String) { val context = LocalContext.current Button(onClick = { Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show() }) { Text("Greet") } }
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 OtherInteractionsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // get data from savedInstanceState setContent { MaterialTheme { ExampleComposable(data, onButtonClick = { startActivity(Intent(this, MyActivity::class.java)) }) } } } } @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 */ }