Migrating to Jetpack Compose

1. Introduction

Compose and the View system can work together side by side.

In this codelab, you'll be migrating parts of the Sunflower's plant details screen to Compose. We created a copy of the project for you to try out migrating a realistic app to Compose.

By the end of the codelab, you'll be able to continue with the migration and convert the rest of Sunflower's screens if you wish.

For more support as you're walking through this codelab, check out the following code-along:

What you will learn

In this codelab, you will learn:

  • The different migration paths you can follow
  • How to incrementally migrate an app to Compose
  • How to add Compose to an existing screen built using Views
  • How to use a View from inside Compose
  • How you can create a theme in Compose
  • How to test a mixed screen written in both Views and Compose

Prerequisites

What you will need

2. Migration strategy

Jetpack Compose was designed with View interoperability right from the start. To migrate to Compose, we recommend an incremental migration where Compose and View co-exist in your codebase until your app is fully in Compose.

The recommended migration strategy is this:

  1. Build new screens with Compose
  2. As you're building features, identify reusable elements and start to create a library of common UI components
  3. Replace existing features one screen at a time

Build new screens with Compose

Using Compose to build new features that encompass an entire screen is the best way to drive your adoption of Compose. With this strategy, you can add features and take advantage of the benefits of Compose while still catering to your company's business needs

A new feature might encompass an entire screen, in which case the entire screen would be in Compose. If you are using Fragment-based navigation, that means you would create a new Fragment and have its contents in Compose.

You can also introduce new features in an existing screen. In this case, Views and Compose will coexist on the same screen. For example, say the feature you are adding is a new view type in a RecyclerView. In that case, the new view type would be in Compose while keeping the other items the same.

Build a library of common UI components

As you're building features with Compose, you'll quickly realize that you end up building a library of components. You'll want to identify reusable components to promote reuse across your app so that shared components have a single source of truth. New features you build can then depend on this library.

Replace existing features with Compose

In addition to building new features, you'll want to gradually migrate existing features in your app to Compose. How you approach this is up to you, but here are a few good candidates:

  1. Simple screens - simple screens in your app with few UI elements and dynamicity such as a welcome screen, a confirmation screen, or a settings screen. These are good candidates for migrating to Compose as it can be done with few lines of code.
  2. Mixed View and Compose screens - screens that already contain a bit of Compose code are another good candidate as you can continue to migrate elements in that screen piece-by-piece. If you have a screen with only a subtree in Compose, you can continue migrating other parts of the tree until the entire UI is in Compose. This is called the bottom-up approach of migration.

Bottom-up approach of migrating a mixed Views and Compose UI to Compose

The approach in this Codelab

In this codelab, you'll be doing an incremental migration to Compose of the Sunflower's plant details screen having Compose and Views working together. After that, you'll know enough to continue with the migration if you wish.

3. Getting set up

Get the code

Get the codelab code from GitHub:

$ git clone https://github.com/android/codelab-android-compose

Alternatively you can download the repository as a Zip file:

Running the sample app

The code you just downloaded contains code for all Compose codelabs available. To complete this codelab, open the MigrationCodelab project inside Android Studio.

In this codelab, you're going to migrate Sunflower's plant details screen to Compose. You can open the plant details screen by tapping in one of the plants available in the plant list screen.

bb6fcf50b2899894.png

Project setup

The project is built in multiple git branches:

  • The main branch is the codelab's starting point.
  • The end contains the solution to this codelab.

We recommend that you start with the code in the main branch and follow the codelab step-by-step at your own pace.

During the codelab, you'll be presented with snippets of code that you'll need to add to the project. In some places, you'll also need to remove code that is explicitly mentioned in comments on the code snippets.

To get the end branch using git, cd into the directory of the MigrationCodelab project followed by using the command:

$ git checkout end

Or download the solution code from here:

Frequently asked questions

4. Compose in Sunflower

Compose is already added to the code you downloaded from the main branch. However, let's take a look at what's required to have it working.

If you open the app-level build.gradle file, see how it imports the Compose dependencies and enables Android Studio to work with Compose by using the buildFeatures { compose true } flag.

app/build.gradle

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

dependencies {
    //...
    // Compose
    def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"
    //...
}

The version of those dependencies are defined in the project-level build.gradle file.

5. Hello Compose!

In the plant details screen, we'll migrate the description of the plant to Compose while leaving the overall structure of the screen intact.

Compose needs a host Activity or Fragment in order to render UI. In Sunflower, as all screens use fragments, you'll be using ComposeView: an Android View that can host Compose UI content using its setContent method.

Removing XML code

Let's start with the migration! Open fragment_plant_detail.xml and do the following:

  1. Switch to the Code view
  2. Remove the ConstraintLayout code and 4 nested TextViews inside the NestedScrollView (the codelab will compare and reference the XML code when migrating individual items, having the code commented out will be useful)
  3. Add a ComposeView that will host Compose code instead with compose_view as view id

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <!-- Step 2) Comment out ConstraintLayout and its children ->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    <!-- End Step 2) Comment out until here ->

    <!-- Step 3) Add a ComposeView to host Compose code ->
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Adding Compose code

At this point, you are ready to start migrating the plant details screen to Compose!

Throughout the codelab, you'll be adding Compose code to the PlantDetailDescription.kt file under the plantdetail folder. Open it and see how we have a placeholder "Hello Compose" text already available in the project.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Surface {
        Text("Hello Compose")
    }  
}

Let's display this on the screen by calling this composable from the ComposeView we added in the previous step. Open PlantDetailFragment.kt.

As the screen is using data binding, you can directly access the composeView and call setContent to display Compose code on the screen. Call the PlantDetailDescription composable inside MaterialTheme as Sunflower uses material design.

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    // ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            // ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        // ...
    }
}

If you run the app, you can see "Hello Compose" displayed on the screen.

66f3525ecf6669e0.png

6. Creating a Composable out of XML

Let's start by migrating the name of the plant. More exactly, the TextView with id @+id/plant_detail_name you removed in fragment_plant_detail.xml. Here's the XML code:

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

See how it has a textAppearanceHeadline5 style, has a horizontal margin of 8.dp and it's centered horizontally on the screen. However, the title to be displayed is observed from a LiveData exposed by PlantDetailViewModel that comes from the repository layer.

As observing a LiveData is covered later, let's assume we have the name available and is passed as a parameter to a new PlantName composable that we create in the PlantDetailDescription.kt file. This composable will be called from the PlantDetailDescription composable later.

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

With preview:

d09fe886b98bde91.png

Where:

  • Text's style is MaterialTheme.typography.headlineSmall which is similar to textAppearanceHeadline5 from the XML code.
  • The modifiers decorate the Text to make it look like the XML version:
  • The fillMaxWidth modifier is used so that it occupies the maximum amount of width available. This modifier corresponds to the match_parent value of the layout_width attribute in XML code.
  • The padding modifier is used so that a horizontal padding value of margin_small is applied. This corresponds to the marginStart and marginEnd declaration in XML. The margin_small value is also the existing dimension resource which is fetched using the dimensionResource helper function.
  • The wrapContentWidth modifier is used to align the text so that it is centered horizontally. This is similar to having a gravity of center_horizontal in XML.

7. ViewModels and LiveData

Now, let's wire up the title to the screen. To do that, you'll need to load the data using the PlantDetailViewModel. For that, Compose comes with integrations for ViewModel and LiveData.

ViewModels

As an instance of the PlantDetailViewModel is used in the Fragment, we could pass it as a parameter to PlantDetailDescription and that'd be it.

Open the PlantDetailDescription.kt file and add the PlantDetailViewModel parameter to PlantDetailDescription:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    //...
}

Now, pass the instance of the ViewModel when calling this composable from the fragment:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

With this, you already have access to the PlantDetailViewModel's LiveData<Plant> field to get the plant's name.

To observe LiveData from a composable, use the LiveData.observeAsState() function.

As values emitted by the LiveData can be null, you'd need to wrap its usage in a null check. Because of that, and for reusability, it's best to split the LiveData consumption and listening in different composables. So let's create a new composable called PlantDetailContent that will display Plant information.

With these updates, the PlantDetailDescription.kt file should now look like this:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent