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 form factor support, and why you might consider a similar approach for your Compose migration.
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.
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 tonon-UI layers. This unblocks parallel migration of entire screens and features.
You have 2 possible approaches:
Keep the View components in parallel to the new Compose design system, essentially maintaining two design systems
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 form factor support!
This migration shows how simple it is to introduce support for form factors once you start using composable elements and screens, and it provides tips and guidance on adopting the right form factors 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.
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 topair Compose with M3to unlock its full potential.
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
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:
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 player and a pro tip.
🏅 MVP player: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 incomingredesign 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 form factors support?
Step 1: Dependencies and theming
First code change is adding Compose dependencies 😅. The Compose setup page has all the info you need.
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 form factors mindset and buildinga reusable, form factor compatible, composable:
Now let’s migrate some XMLs to Compose!
search_suggestion_item.xml is a ConstraintLayout with an ImageView and two TextViews:
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 shows an interesting interop capability.
The original XML is a Material Chip component which is added to a ChipGroup in ComposeFragment:
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:
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?
Another proof of “Interop just works!” is the 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. 😈
🏅 MVP player: 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 layouts — FlowRow or FlowColumn: