Migrating to Jetpack Compose — an interop love story [part 1]

Simona Milanović
Android Developers
Published in
12 min readSep 21, 2023

--

This blog series covers a “common UI first” migration strategy of a View sample to Jetpack Compose, guiding you through the steps, with interop and large screens support, and why you might consider a similar approach for your Compose migration.

Intro

Most of you are probably familiar with Jetpack Compose and its benefits (simplifies-and-accelerates-UI-Android-development-with-less-code-powerful-tools-and-intuitive-Kotlin 🎶), so we won’t talk about this. If you’re able to start anew and create a Compose-only app, you’re on the right track. But then, this blog might not be for you :).

While Compose-only is a dream experience, the reality is that existing apps will be mixed Views and Compose for a long time. In fact, the most common Compose migration strategy we’ve seen among apps is: all new features and screens written in Compose, while old code remains in Views. Depending on capacities, developers refactor old code as well. But it’s not a must.

This is important when deciding to start migrating to Compose. You don’t need a Compose-only app to reap the benefits of Compose. Mixed View and Compose code can work together and the sooner you introduce Compose, the easier your maintenance will be in the long run.

The future of Android UI is Compose-only. But the present requires Compose interop with Views.

With that in mind, let’s embark on a journey of migrating a View sample without going all the way to 100% Compose to test interop capabilities, and a few more assumptions. To see a 100% Compose migration, check out Migrating Sunflower to Jetpack Compose.

Goals of this migration

The starting point is our sample protagonist Reply which is View-based and using Material 2. The hypotheses to test during this migration were:

Testing the “Migration of common UI first” strategy

Chances are you’re working in a team, so as we all know, parallelizing development is crucial for efficiency and minimizing duplicate work. This migration shows that common UI components, shared UI code or, on a larger scale, design systems can be migrated first, with minimal to no changes to non-UI layers. This unblocks parallel migration of entire screens and features.

You have 2 possible approaches:

  1. Keep the View components in parallel to the new Compose design system, essentially maintaining two design systems
  2. Migrate entirely to a single Compose design system, and while doing so, gradually update existing View code to use these new components

For the goals of this sample, we chose to migrate all common UI and use it in existing View code.

Note: As Compose is an unbundled framework, adding it for the first time has a loading cost. For more information, see the Why should you always test Compose performance in release? blog post. Use Macrobenchmarks to measure performance and make an informed decision between the two options above.

Testing interop capabilities

Our test subject is an elderly sample, so it won’t be getting new features. Therefore, we opted to migrate existing screens to Compose, rather than adopt Compose for new screens, as both approaches test the Compose and View interop capabilities. Validating the power of interop shows that refactoring old code is not a must and that incremental migration to Compose works well. You can if you want to — but it’s not a necessity and you can rely on interop.

Migrating to Compose while keeping the original navigation architecture

This migration shows that you can keep using Jetpack Navigation or a custom navigation setup, even for screens that are entirely Compose, making incremental migration to Compose less work. Using Navigation Compose isn’t a requisite to use Jetpack Compose.

Redesigning a feature/screen? Perfect time to migrate to Compose!

If a major redesign of a feature or a screen is incoming, this might be a good time to board the Compose train. You will have to do some rewriting anyway, so why not take this opportunity and do it with Compose?

Migrating to Compose? Perfect time to add large screens support!

This migration shows how simple it is to introduce support for large screens once you start using composable elements and screens, and it provides tips and guidance on adopting the right large screens mindset from start.

Views to Compose don’t always need a 1:1 mapping

If you’ve used one component in Views, you might not need the same component in Compose. Take advantage of migration to rethink the implementation of your elements — change them or simplify them, if needed. This sample shows examples of how you can switch from, for example, View ConstraintLayouts to Compose Rows and Columns instead.

Brief overview of Reply

Reply is an elderly sample. It’s a simple email app, with basic functionality of displaying emails, filtering, organizing, and navigating between screens. It consists of Search, Home, Email and Compose (composing an email) screens.

Note: There is a newer Reply version, with Compose and M3, as part of our official Compose samples.

As with most samples, it focuses on showcasing a singular concept — in this case the Material components — and simplifies the rest. So you might notice it doesn’t have a ViewModel, but rather a simulation of business logic in the EmailStore. It also doesn’t have any tests 😬. This makes it quite different from a production app (hopefully!). But as samples are oversimplified by design, this will have to do.

Note: Reply uses M2. Rather than migrating to Compose and M3 at the same time, we chose to split it into two tasks to promote an incremental and safe migration process. Reply M3 migration is planned for a V2 sequel, along with navigation and animation. However, we encourage you to pair Compose with M3 to unlock its full potential.

Captain’s migration log

This log follows the steps we took when migrating Reply. However, everyone’s free to choose their own path! Use this as a guide, rather than a rule book:

Step 0: Migration prep [in this post]
Step 1: Dependencies and theming [in this post]
Step 2: Smallest common UI components [in this post]
Step 3: Migration of more complex components [in part 2]
Step 4: Migration of low risk screens [in part 2]
Step 5: Migration of more complex screens [in part 2]

Step 0: Migration prep

🗒️ Tasks:

  • Read migration documentation
  • Analyze and test the app
  • Verify Compose prerequisites
  • Divide and conquer the workload

Read migration documentation

A beginning is the time for taking the most delicate care that the balances are correct”, Frank Herbert said wisely. Meaning — it’s important to do your preparation well, before actually starting. Migrating from Views to Compose can be a mammoth task — it takes time, and you should prioritize safety and incrementation over speed. Presuming you have basic knowledge of Compose, the following materials make a good migration starter pack:

Verify Compose prerequisites

Make sure that your app is in a good position for Compose migration. Consider the following requirements:

  • Is it applying or equipped to apply UDF? ✅
  • Are there any structural blockers — e.g., minSdk<21, not using Kotlin 😳? ❌
  • Are any of your frameworks (testing, navigation etc.) too tightly coupled with View APIs? ❌

As well as the optional, nice-to-haves:

  • Is the app following the best architecture practices? ✅
  • How good is your team’s knowledge of basic Compose? ✅ 😎
  • Do you have the time and resources for the learning curve? ✅

For Reply, nothing required drastic changes before adding Compose.

Divide and conquer the workload

You want the initial migration steps to unblock others ASAP so they can develop new features and screens in Compose. For that, you need a plan for migrating the common UI first — whether it’s part of a grander design system, in a common UI module, or simply stacked in a separate folder. This is the bottom-up migration strategy.

In Reply, there are only two shared classes in ui/common, so we used a little imagination and migrated some additional ones — components equipped to be included in another XML layout or reused via its binding. Play pretend for the sake of the end goal 🙂.

Now that we have studied, analyzed, and come up with a plan, it’s time to start coding.

Every migration step has an MVP and a pro tip.

🏅 MVP: Screenshot testing. A crucial thing to facilitate, as migration from Views to Compose could bring some difficult-to-catch UI regression. To learn more about Compose and screenshot testing, take a look at how this was added to Now in Android.

💎 Pro tip: Another great opportunity for Compose migration is when there’s an incoming redesign of a feature, screen, or big chunk of components. And if all this rewrite work is being done, why not get the additional benefit of introducing large screens support?

Step 1: Dependencies and theming

🗒️ Tasks:

  • Add dependencies
  • Migrate theme

First code change is adding Compose dependencies 😅. The Compose setup page has all the info you need.

Theming would often be the second code change. Since setting up theming is basic Compose knowledge, we initially decided to use the mdctheme adapter to quickly maps the XML theming to a corresponding Compose one. However, the adapter has since been deprecated and our guidance now recommends setting up the M2 theming manually, following the Compose M2 theming codelab.

Note: Material 3 is our recommended design system. See Migrate XML themes to Compose and how to use the Material 3 Theme Builder for more information.

💻 This step consists of two PRs, done at different times:

Step 1 — Compose dependencies [With the mdctheme adapter code]

Step 1.2 — M2 theming setup [Removal of deprecated mdctheme adapter and manual setup of M2 theme]

🏅 MVP: Material 3 Theme Builder. This tool helps you visualize Material dynamic color, easily create a custom M3 theme, and export it to code.

💎 Pro tip: A safe and incremental Compose migration follows this order:

  1. Smallest common components first
  2. Scale to more complex common components
  3. Low-risk, low-complexity screens and features
  4. Scale to high-engagement screens and features

Step 2: Smallest common UI components

🗒️ Tasks:

  • Find smallest common elements
  • Design for large screens at component-level
  • Migrate elements

Find smallest common elements

In Reply, I looked for the smallest, reused XMLs and elements, without a lot of binding variables, least data pulled from the data layer, and least dependencies needed (like Glide, for example):

account_item_layout.xml
compose_recipient_chip.xml
search_suggestion_title.xml and search_suggestion_item.xml

Design for large screens at component-level

Once you’ve flagged the components, try to think about them in different screen sizes. An important concept here is the difference between app, screen and component-level composables and where your components fit:

  • App-level composable: The single, root composable that occupies all space given to your app, and contains all other composables.
  • Screen-level composable: A composable contained within the app-level composable that occupies all space given to your app, each generally representing a particular destination when navigating.
  • Individual composables (component-level): All other composables — individual elements, reusable groups of content, or composables hosted within screen-level composables.

This step will be based on individual composables. On this level, think about how composables should occupy the real estate they’re being given, rather than being tied to fixed, screen-reliant values. Composables should be able to fit well in any area they’re in. This means relying on modifiers like fillMaxWidth, weights, and making composables fully reusable and customizable.

This is where bottom-up migration works best — you build your smallest, adjustable components first, and then gradually think larger. Screen-level composables can then rearrange these smaller components based on the available space, screen sizes, and designs.

In general, we advise that your composables follow our API guidelines. There are some additional points important for setting the right large screens mindset and building a reusable, large screens compatible, composable:

Now let’s migrate some XMLs to Compose!

Migrate elements

search_suggestion_item.xml is a ConstraintLayout with an ImageView and two TextViews:

search_suggestion_item.xml

Easy peasy. While ConstraintLayout exists in Compose, remember you don’t always need a 1:1 mapping — you can simplify your implementation with Compose. That’s how an XML ConstraintLayout with constraints for each element can turn into a simple combo of Rows and Columns in Compose:

Being a good Compose scout 🫡, for every new composable, we also add a Preview. Since Reply has a light and dark theme, we take advantage of multipreviews to preview SearchSuggestionItem in both:

Note: In Compose 1.6.0-alpha05 version, PreviewLightDark multipreview annotation was added, to avoid the manual creation above.

Now we’re ready to hook the first composable in. In SearchFragment, we replace the old code:

With the new one:

A few things to break down here:

search_suggestion_title.xml item and its SearchSuggestionHeader composable follow very similar steps, so let’s ⏩ that.

compose_recipient_chip.xml

compose_recipient_chip.xml shows an interesting interop capability.

compose_recipient_chip.xml

The original XML is a Material Chip component which is added to a ChipGroup in ComposeFragment:

recipientChipGroup

The ChipGroup acts as a FlowLayout container for Chips, fitting as many Chips as possible in one row and can overflow to the next.

This XML is simply replaced with a Chip composable:

To add it to the ComposeFragment, we take full advantage of the interop power and simply replace the old View Chip with the Compose one, directly adding it to the View ChipGroup:

Before:

After:

We’re can add a Chip composable to a View ChipGroup without any other changes. Even the more complex logic of expandChip(), which handles chip’s expanding and collapsing transformation, still works the same way with the Chip composable. How cool is that?

expandChip()

Another proof of “Interop just works!” is the account_item_layout.xml.

account_item_layout.xml

This was used as a RecyclerView item:

Once this element is migrated to AccountItem composable, we just replace it in the original callsite and … it works!

Note: Make sure you are using the latest versions of RecyclerView and Compose to ensure they are performant together. See Using Compose in a RecyclerView to learn more.

As a reward, you get to do some fun removal of old View code lines, XML files, styling and theming resources and attributes, custom binding adapter behaviors, etc. 😈

💻 PR: Step 2

🏅 MVP: Flow layouts. Remember, Compose migration isn’t always a 1:1 mapping to Views. Since ChipGroup is just a more opinionated FlowLayout, you could consider replacing it with Compose Flow layoutsFlowRow or FlowColumn:

Note: Compose 1.4.0+ introduced Flow layouts. In earlier versions, refer to Accompanist Flow layouts instead.

💎 Pro tip: For large parameter lists in previews, use the PreviewParameter annotation for better readability.

To be continued…

Phew, let’s pause here 🎬 ! We’ve covered A LOT in this post:

  • The intro and goals of this migration
  • Brief overview of the migration sample
  • First three steps of the captain’s log: 1. migration prep, 2. dependencies & theming, 3. migration of smallest common UI

In part 2️⃣, we continue our migration journey 🚢 and summarize our takeaways. Stay tuned!

Migrating to Jetpack Compose — an interop love story [part 1]
Migrating to Jetpack Compose — an interop love story [part 2]

--

--

Simona Milanović
Android Developers

Android Developer Relations Engineer @Google, working on Jetpack Compose