Techbase Academy
Android · Lesson 43

Compose and Views Interoperability

11 phasesMigrate One ScreenAndroid Studio
Phase 1 of 11
Lesson Introduction

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 with ViewCompositionStrategy.
  • Embed a classic View in Compose with AndroidView, and a Fragment with AndroidViewBinding.
  • 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.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts
  • 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).
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: Two Worlds, One Screen

There are two directions of interop:

text
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.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Two Worlds, One Screen. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: Add Compose to a View Project

Start with the ClassRegister project from lesson 42 (or any View-based project). Its app/build.gradle.kts needs four things:

  1. The Kotlin Compose compiler plugin (since Kotlin 2.0).
  2. buildFeatures { compose = true }.
  3. The Compose BOM and the libraries you use.
  4. 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):

libs.versions.toml
[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:

build.gradle.kts
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).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Add Compose to a View Project. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: Compose Inside an XML Layout

Add a ComposeView to fragment_detail.xml (from the lesson 42 solution). It is an ordinary View tag:

fragment_detail.xml
<?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:

DetailFragment.kt
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:

kotlin
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:

kotlin
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.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Compose Inside an XML Layout. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: Views Inside Compose with AndroidView

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):

kotlin
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:

text
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:

kotlin
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:

kotlin
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):

kotlin
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.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: Views Inside Compose with AndroidView. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: Sharing State and Theme

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:

  1. Re-implement the XML theme in Compose by hand (copy the same colours into Color.kt).
  2. 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 Compose MaterialTheme automatically. 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).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Sharing State and Theme. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Step-by-Step Build 5: A Migration Strategy That Works

The official Migrate from the View system material gives a practical order of work. Here is our own version, in six steps.

text
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.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: A Migration Strategy That Works. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

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:

kotlin
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.

🎯 Your Challenge

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.

kotlin
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.

🎯 Your Challenge

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:

  1. Add Compose and a Compose theme matching the XML theme. Risk: Kotlin/Compose compiler mismatch, APK size increase.
  2. Build the next new screen in Compose inside a fragment (ComposeView). Risk: wrong ViewCompositionStrategy loses state on back navigation.
  3. Migrate leaf UI (list rows, dialogs, empty states) and simple screens (About, Settings). Risk: two themes drift apart.
  4. Move state into ViewModels for the remaining screens.
  5. Migrate the home feed to LazyColumn (keys and content types). Risk: scroll position and performance regressions.
  6. Keep MapView in AndroidView (or move to the maps Compose library if it meets your needs).
  7. Swap navigation to Navigation Compose when most screens are Compose.
  8. Remove XML, adapters and the theme bridge.
✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

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.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

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

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Migrate One Screen

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:

  1. Add Compose to the project (plugin, BOM, compose = true) and confirm it builds.
  2. Create Theme in Compose with the same primary colour as the XML theme.
  3. Convert DetailFragment to return a ComposeView with the detail UI (name, present switch that updates the ViewModel).
  4. Replace the header of ClassFragment with a ComposeView showing the present count.
  5. Add a StarRating AndroidView to the detail screen (a "behaviour rating" stored in the ViewModel).
  6. 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:

StudentViewModel.kt
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:

DetailFragment.kt
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.

1

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.

2

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”.

3

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.

4

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.

5

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
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close