Most real Android apps are not purely Compose or purely Views. They are both, and they stay that way for months or years while the team migrates a screen at a time. Google designed Compose for exactly this: you can put Compose inside a View-based screen, put a View inside a Compose screen, and move over gradually without rewriting the whole app.
This lesson covers these items: Compose in Views and Add Compose to a legacy app (Unit 8, Pathway 2 of Android Basics with Compose), and Views in Compose, View Interoperability in Compose, Migrate from the View system and the hands-on migration codelab (Migrating to Jetpack Compose) from Pathway 1 of the Jetpack Compose for Android developers programme. See https://developer.android.com/courses/pathways/android-basics-compose-unit-8-pathway-2 and https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1.
By the end you will be able to:
- Add Compose to an existing View-based project (Gradle setup).
- Embed Compose in an XML layout with
ComposeView, and in a Fragment correctly withViewCompositionStrategy. - Embed a classic View in Compose with
AndroidView, and aFragmentwithAndroidViewBinding. - Share state and theming between the two worlds.
- Explain and apply a safe, incremental migration strategy.
- Recognise the common interop bugs: lifecycle, state loss, and theming mismatches.
Big idea: a migration is a series of small, shippable steps, never a big rewrite. At every step the app builds, runs and can be released.
- Views, XML, view binding, Fragments, Navigation component (lesson 42).
- Compose basics, state and recomposition (lessons 15–21 and 37).
- ViewModel and StateFlow (lessons 24–25).
- Material 3 theming (lesson 40).
There are two directions of interop:
Direction A: Compose inside Views Direction B: Views inside Compose
Activity / Fragment (XML) Activity (setContent { })
+-- LinearLayout +-- Column
+-- TextView +-- Text (Compose)
+-- ComposeView <--- Compose lives here +-- AndroidView <--- a View lives here
Text, Button ... MapView / WebView / custom View
Direction A is how most migrations start: leave the Activity and navigation alone, replace one small piece of a screen with Compose, then grow it.
Direction B is how you keep using things that have no Compose version yet or are too costly to rewrite: WebView, MapView, ad banners, video players, a complex custom View.
Under the hood, a ComposeView is just a View (a ViewGroup, in fact) that owns a Composition. A Composition needs a lifecycle, a saved-state registry and a ViewModelStore to work. In an Activity or Fragment these are found automatically from the view tree. That is why correct setup (and the ViewCompositionStrategy) matters.
Start with the ClassRegister project from lesson 42 (or any View-based project). Its app/build.gradle.kts needs four things:
- The Kotlin Compose compiler plugin (since Kotlin 2.0).
buildFeatures { compose = true }.- The Compose BOM and the libraries you use.
- A Compose-ready theme (optional but recommended).
In gradle/libs.versions.toml, add (look up the current stable versions: the Compose BOM and the Kotlin version should match what Android Studio's New Project creates):
[versions]
composeBom = "<latest stable>"
kotlin = "<your Kotlin version>"
[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-viewbinding = { group = "androidx.compose.ui", name = "ui-viewbinding" }
[plugins]
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Add the plugin to the root build.gradle.kts (alias(libs.plugins.kotlin.compose) apply false) and to the app module:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
// ...
buildFeatures {
viewBinding = true
compose = true
}
}
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.viewbinding) // for AndroidViewBinding later
// ...existing dependencies
}
Sync. If Gradle complains about the Kotlin version, remember: the Compose compiler is now shipped with Kotlin and its plugin version must equal your Kotlin version. This is a common first error when adding Compose to an old project that still uses the pre-2.0 composeOptions { kotlinCompilerExtensionVersion } setup; remove that block when switching to the plugin.
Make sure your project's minSdk meets the Compose requirement shown in the official documentation (Compose has a minimum API level; check the current figure in the setup page).
Add a ComposeView to fragment_detail.xml (from the lesson 42 solution). It is an ordinary View tag:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/detailName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28sp"
android:textStyle="bold" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/statusCompose"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Then in the fragment, give it content. A ComposeView has a setContent { } function, exactly like an Activity:
package com.example.classregister
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.classregister.databinding.FragmentDetailBinding
class DetailFragment : Fragment() {
private var _binding: FragmentDetailBinding? = null
private val binding get() = _binding!!
private val viewModel: StudentViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = requireArguments().getInt("studentId")
binding.statusCompose.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
val students by viewModel.students.collectAsStateWithLifecycle()
val student = students.firstOrNull { it.id == id }
if (student != null) StatusCard(student)
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@Composable
private fun StatusCard(student: Student) {
Card(Modifier.padding(top = 16.dp)) {
Column(Modifier.padding(16.dp)) {
Text(student.name, style = MaterialTheme.typography.titleLarge)
Text(if (student.present) "Present today" else "Absent today")
}
}
}
(If you also have the old code that sets detailStatus text, delete it; we replaced that view with Compose.)
The new import you have not seen is androidx.lifecycle.compose.collectAsStateWithLifecycle, which needs the androidx.lifecycle:lifecycle-runtime-compose artifact (lesson 25).
ViewCompositionStrategy: when does the Composition die?
A Composition must be disposed at the right time to avoid leaks, but the right time depends on where the ComposeView lives. This is the single most important rule of interop. ViewCompositionStrategy chooses it:
| Strategy | Disposes when | Use in |
|---|---|---|
DisposeOnDetachedFromWindowOrReleasedFromPool |
The View is detached from the window (default; pool-aware) | Activities, simple Views, and RecyclerView items |
DisposeOnViewTreeLifecycleDestroyed |
The LifecycleOwner of the view tree is destroyed |
Fragments: use the fragment's view lifecycle |
DisposeOnLifecycleDestroyed(lifecycle) |
A given lifecycle is destroyed | Custom cases |
DisposeOnDetachedFromWindow |
Detached from the window (non-pool-aware variant) | Rarely, specific containers |
For a ComposeView inside a Fragment, choose DisposeOnViewTreeLifecycleDestroyed. Fragments detach their views when you navigate away but keep the fragment on the back stack, and the default strategy would drop the Composition and its remembered state. For items in a RecyclerView, the default pool-aware strategy is intended.
When the fragment itself is just Compose
If the whole fragment is Compose, return a ComposeView directly from onCreateView:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
class AboutFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme { Text("About Class Register") }
}
}
}
This is the standard shape of a Compose screen inside a Navigation-component graph, and it is the best first migration step: keep fragments and navigation, but write each screen's content in Compose.
Compose inside a RecyclerView item
You can migrate one list row at a time. In the view holder, wrap a ComposeView:
import androidx.compose.material3.Text
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.recyclerview.widget.RecyclerView
class ComposeStudentHolder(private val composeView: ComposeView) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bind(student: Student) {
composeView.setContent {
Text(student.name)
}
}
}
Keep Compose rows light: a RecyclerView creates many holders, and each is its own Composition. This works, but for a whole list it is usually better to migrate the entire list to LazyColumn than to keep the adapter.
Now the reverse direction. AndroidView lets you place any View into a composable. It takes three main parameters:
| Parameter | Runs | Purpose |
|---|---|---|
factory |
Once, when the View is created | Create and configure the View (needs a Context) |
update |
On first composition and every time its state reads change | Push new state into the View |
onRelease (optional) |
When the View leaves the composition | Clean up |
Example: show a rating bar from the View system (RatingBar, which has no direct Material 3 Compose counterpart in the base library):
import android.widget.RatingBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun StarRating(
rating: Float,
onRatingChange: (Float) -> Unit,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier,
factory = { context ->
RatingBar(context).apply {
numStars = 5
stepSize = 0.5f
setOnRatingBarChangeListener { _, value, fromUser ->
if (fromUser) onRatingChange(value)
}
}
},
update = { bar ->
if (bar.rating != rating) bar.rating = rating
}
)
}
How state flows:
Compose state (rating) --> update { bar.rating = rating } (state flows DOWN into the View)
View event (user taps) --> listener --> onRatingChange(...) (events flow UP to Compose state)
That is the same state down, events up pattern as everywhere else. The if (bar.rating != rating) check avoids setting a value that is already there, which would trigger the listener and cause a loop.
A common trap: the lambda in factory captures onRatingChange the first time only, because factory runs once. If your callback changes later (a new lambda each recomposition), the View keeps calling the old one. The safe fix is rememberUpdatedState or setting the listener again in update:
update = { bar ->
bar.setOnRatingBarChangeListener { _, value, fromUser ->
if (fromUser) onRatingChange(value)
}
if (bar.rating != rating) bar.rating = rating
}
A WebView in Compose
WebView is the classic reason to use AndroidView:
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun SimpleWebPage(url: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
webViewClient = WebViewClient()
settings.javaScriptEnabled = false // enable only if you trust and need it
}
},
update = { webView ->
if (webView.url != url) webView.loadUrl(url)
}
)
}
Remember the INTERNET permission in the manifest (lesson 29), and enable JavaScript only for content you trust. For production-quality web content, also look at the androidx.webkit library for safe-browsing and dark-mode support.
AndroidView and lifecycle-heavy Views
Views such as MapView and PlayerView need lifecycle calls (onResume, onPause, onDestroy). In Compose you forward them with a LifecycleEventObserver inside a DisposableEffect (lesson 54), or use the library's own Compose wrapper if it has one (Maps and media libraries now ship Compose APIs; prefer those).
Fragments and XML layouts inside Compose
If you have a whole XML layout or a fragment container that you do not want to rewrite yet, use AndroidViewBinding (needs ui-viewbinding and view binding enabled):
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidViewBinding
import com.example.classregister.databinding.LegacyHeaderBinding
@Composable
fun LegacyHeader(title: String, modifier: Modifier = Modifier) {
AndroidViewBinding(LegacyHeaderBinding::inflate, modifier) {
headerTitle.text = title // 'this' is the generated binding
}
}
The trailing lambda runs on every recomposition with the binding as its receiver, so it is where you bind data. A FragmentContainerView inside such a layout lets you host an old fragment inside a Compose screen.
State
The cleanest bridge is a ViewModel exposing a StateFlow. A Fragment's View code can collect it (lesson 42) and a Compose screen can collectAsStateWithLifecycle(). Both see the same data, so you can move parts of a screen to Compose without changing the data layer at all. Other bridges: LiveData.observeAsState() (needs the androidx.compose.runtime:runtime-livedata artifact) if your old code uses LiveData, and Flow.collectAsState().
Theme
Compose reads its colours and type from MaterialTheme; Views read from the XML theme (Theme.Material3.* in themes.xml). If they differ, your screens look like two different apps. Two strategies:
- Re-implement the XML theme in Compose by hand (copy the same colours into
Color.kt). - Use the Material Theme Adapter library (
com.google.android.material:compose-theme-adapter, or the M3 variant) that reads your XML theme and creates a ComposeMaterialThemeautomatically. It is useful during a migration, but treat it as a bridge rather than a long-term solution: check the library's page for its current status, and plan to move to a native Compose theme.
If a Compose screen sits in an Activity with a Material 3 XML theme, also make sure <item name="android:windowBackground"> and status bar colours are consistent with what Compose draws.
Sizes, insets and dark mode
Mixed screens commonly have two bugs: double insets (the XML layout adds padding for the status bar and Compose adds it again) and dark mode mismatch. Decide once which layer owns window insets (modern apps call enableEdgeToEdge() in the Activity and handle insets in the layout system or in Compose, not in both).
The official Migrate from the View system material gives a practical order of work. Here is our own version, in six steps.
STEP 1 Add Compose to the project (Gradle, theme). Ship nothing yet.
STEP 2 Build ONE new, small screen in Compose. Host it in a Fragment (ComposeView).
STEP 3 Migrate "leaf" UI first: buttons, cards, list rows, dialogs, empty states.
STEP 4 Migrate whole screens (one fragment at a time), moving state into ViewModels.
STEP 5 Replace the navigation (Navigation component -> Navigation Compose) once most
screens are Compose.
STEP 6 Remove XML, view binding, the Adapter classes and the theme bridge.
Guidelines that save pain:
- Start with a screen that has little navigation or state complexity (an About page, a settings list, a detail screen).
- Migrate bottom-up (leaf to root) or top-down by whole screens, but not both at once.
- Move logic to the ViewModel first. A screen whose state already lives in a ViewModel migrates in an afternoon.
- Keep tests green at every step. UI tests with Espresso can still find Views inside a
ComposeView; Compose test APIs can find composables inside View screens (lesson 45). - Do not start by redoing your theme or design system twice. Define Material 3 in Compose from the start.
- Measure. Check start-up time and release-build performance after adding Compose, because Compose adds some library weight (lesson 57).
The hands-on migration codelab
The official Migrating to Jetpack Compose codelab migrates a real app (Sunflower) step by step: a detail screen, the plant name, the description with HtmlText, theming and the "add to garden" button. You can follow it with the links below. The skills it asks for are exactly the ones in this lesson: ComposeView in a fragment, AndroidView for HTML text, the theme adapter and state from a ViewModel.
Exercise 1: Replace one row
In the lesson 42 ClassRegister project, replace the title TextView at the top of the main fragment with a ComposeView showing "Class Register" and the count of present students, e.g. "Class Register (3 present)". The count must update when you tick boxes.
Add a ComposeView with id headerCompose to fragment_class.xml in place of the TextView, then in onViewCreated:
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.getValue
binding.headerCompose.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
val students by viewModel.students.collectAsStateWithLifecycle()
val present = students.count { it.present }
Text(
"Class Register ($present present)",
style = MaterialTheme.typography.headlineSmall
)
}
}
}
The View-based list and the Compose header observe the same StateFlow, so they stay in sync.
Exercise 2: Compose screen hosting a View
Write a composable HelpScreen that shows a heading in Compose and, below it, a WebView loading https://developer.android.com/courses. Explain which part runs once and which part runs on each recomposition.
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun HelpScreen(url: String = "https://developer.android.com/courses") {
Column(Modifier.padding(16.dp)) {
Text("Help", style = MaterialTheme.typography.headlineMedium)
AndroidView(
modifier = Modifier.padding(top = 12.dp),
factory = { context ->
WebView(context).apply { webViewClient = WebViewClient() }
},
update = { webView ->
if (webView.url != url) webView.loadUrl(url)
}
)
}
}
factory runs once when the WebView is created. update runs after the first composition and again whenever state it reads (here url) changes. The manifest needs the INTERNET permission.
Exercise 3: Write a migration plan
You inherit an app with 12 Fragment screens, an XML theme, a RecyclerView-based home feed and a MapView on one screen. In 6–8 lines, write the order you would migrate in and one risk for each of the first three steps.
A reasonable plan:
- Add Compose and a Compose theme matching the XML theme. Risk: Kotlin/Compose compiler mismatch, APK size increase.
- Build the next new screen in Compose inside a fragment (
ComposeView). Risk: wrongViewCompositionStrategyloses state on back navigation. - Migrate leaf UI (list rows, dialogs, empty states) and simple screens (About, Settings). Risk: two themes drift apart.
- Move state into ViewModels for the remaining screens.
- Migrate the home feed to
LazyColumn(keys and content types). Risk: scroll position and performance regressions. - Keep
MapViewinAndroidView(or move to the maps Compose library if it meets your needs). - Swap navigation to Navigation Compose when most screens are Compose.
- Remove XML, adapters and the theme bridge.
Mistake #1: Old composeOptions with the new compiler plugin. Symptom: Gradle errors about the compiler extension version. Fix: remove composeOptions, apply the org.jetbrains.kotlin.plugin.compose plugin.
Mistake #2: Default ViewCompositionStrategy in a Fragment. Symptom: state lost when you navigate back. Fix: DisposeOnViewTreeLifecycleDestroyed.
Mistake #3: Forgetting MaterialTheme around Compose content in a ComposeView. Symptom: components use the default baseline theme. Fix: wrap content in your theme.
Mistake #4: Putting a long-lived object creation in update. Symptom: leaks and slowness. Fix: create in factory, update in update.
Mistake #5: Stale callbacks in factory. Symptom: clicks call an old lambda. Fix: set listeners in update, or use rememberUpdatedState.
Mistake #6: Double window insets. Symptom: extra gap under the status bar. Fix: let one layer own insets.
Mistake #7: Two themes that drift apart. Symptom: buttons in two styles. Fix: share the same colours (or a bridge during the migration).
Mistake #8: A ComposeView inside a scrolling parent without a size. Symptom: zero height or measure crash for lazy lists. Fix: give the ComposeView a bounded height or avoid nesting lazy lists in View scrollers.
Mistake #9: Big-bang rewrite. Symptom: months with no release. Fix: migrate screen by screen and ship each time.
Mistake #10: Forgetting ui-tooling for previews. Symptom: @Preview does not show. Fix: add debugImplementation for ui-tooling and implementation for ui-tooling-preview.
Question 1. What does ComposeView do?
It is a View you can place in XML (or create in code) that hosts a Compose Composition. You call setContent { } on it to supply composables.
Question 2. Which ViewCompositionStrategy should a ComposeView in a fragment use, and why?
DisposeOnViewTreeLifecycleDestroyed, because the fragment's view lifecycle matters. The default strategy disposes when the view is detached, which loses remembered state when the fragment goes onto the back stack.
Question 3. What is the difference between factory and update in AndroidView?
factory runs once to create the View. update runs after creation and whenever state it reads changes, so it pushes new data into the existing View.
Question 4. Name two ways to share data between View-based and Compose-based parts of one screen.
A shared ViewModel exposing StateFlow (collected in both worlds), and adapters such as LiveData.observeAsState() or Flow.collectAsStateWithLifecycle() in Compose.
Question 5. What is a safe order for migrating an existing app?
Add Compose and a theme, build new screens or small pieces in Compose inside fragments, migrate leaf UI and simple screens, move state to ViewModels, then replace navigation and remove XML at the end, shipping at every step.
Go deeper: official resources
- Android Basics with Compose, Unit 8 Pathway 2
- Jetpack Compose for Android developers, Pathway 1
- Migrating to Jetpack Compose codelab
- Jetpack Compose course page
Project goal: Take the Views-based Class Register from lesson 42 and migrate it one step at a time, so that after each step the app still runs.
Requirements:
- Add Compose to the project (plugin, BOM,
compose = true) and confirm it builds. - Create
Themein Compose with the same primary colour as the XML theme. - Convert
DetailFragmentto return aComposeViewwith the detail UI (name, present switch that updates the ViewModel). - Replace the header of
ClassFragmentwith aComposeViewshowing the present count. - Add a
StarRatingAndroidViewto the detail screen (a "behaviour rating" stored in the ViewModel). - Write down the next three migration steps for this app.
Stretch goal: convert the RecyclerView list to a LazyColumn inside a ComposeView and delete StudentAdapter.
Add rating to the ViewModel:
package com.example.classregister
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
data class Student(
val id: Int,
val name: String,
val present: Boolean = false,
val behaviour: Float = 3f
)
class StudentViewModel : ViewModel() {
private val _students = MutableStateFlow<List<Student>>(emptyList())
val students: StateFlow<List<Student>> = _students.asStateFlow()
private var nextId = 1
fun add(name: String) { _students.update { it + Student(nextId++, name) } }
fun setPresent(id: Int, present: Boolean) = change(id) { it.copy(present = present) }
fun setBehaviour(id: Int, rating: Float) = change(id) { it.copy(behaviour = rating) }
private fun change(id: Int, transform: (Student) -> Student) {
_students.update { list -> list.map { if (it.id == id) transform(it) else it } }
}
}
(Remove the older data class Student from StudentAdapter.kt so there is only one definition.) The detail fragment as a Compose screen:
package com.example.classregister
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
class DetailFragment : Fragment() {
private val viewModel: StudentViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val id = requireArguments().getInt("studentId")
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
val students by viewModel.students.collectAsStateWithLifecycle()
val student = students.firstOrNull { it.id == id }
if (student != null) {
Column(Modifier.padding(24.dp)) {
Text(student.name, style = MaterialTheme.typography.headlineMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Present")
Switch(
checked = student.present,
onCheckedChange = { viewModel.setPresent(id, it) },
modifier = Modifier.padding(start = 12.dp)
)
}
Text("Behaviour rating", Modifier.padding(top = 16.dp))
StarRating(
rating = student.behaviour,
onRatingChange = { viewModel.setBehaviour(id, it) }
)
}
}
}
}
}
}
}
StarRating is the AndroidView composable from Build 3 (place it in StarRating.kt in the same package). The fragment no longer needs fragment_detail.xml or a view binding, so delete both.
The next three migration steps: (1) convert ClassFragment to a ComposeView with a LazyColumn and delete the adapter and item_student.xml; (2) move navigation to Navigation Compose and make MainActivity use setContent { }; (3) remove view binding, the XML theme duplicates and unused layouts.
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 43 project”.
On github.com click New repository, name it android-lesson-43-compose-and-views-interoperability, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 43 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: Two Worlds, One Screen
- Step-by-Step Build 1: Add Compose to a View Project
- Step-by-Step Build 2: Compose Inside an XML Layout
- Step-by-Step Build 3: Views Inside Compose with AndroidView
- Step-by-Step Build 4: Sharing State and Theme
- Step-by-Step Build 5: A Migration Strategy That Works
- Guided Practice Exercises
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check