At ADS ’22, I shared a migration strategy from existing View-based apps to Compose. In this blog post, we take a look at how to apply that strategy in practice by completing the migration of the Sunflower sample app to Compose.
Before jumping into how we went about migrating the app, it’s helpful to “set the scene” and see what the starting point was for Sunflower before we started the migration.
Sunflower initially was started as a sample app to showcase best practices for several Jetpack libraries such as Jetpack Navigation, Room, ViewPager2, and more. It uses Material Design 2 as the design language and has 5 distinct screens wherein each screen is represented as a Fragment. Sunflower was actually already partly written in Compose — the plant details screen was reimplemented in Compose.
Given this starting point, the next step is to devise a plan to migrate the rest of the app. Note that the focus of this blog post is to migrate Sunflower to Compose — migrating to Material 3 is left as a separate task.
The migration strategy to Compose can be summarized in these steps:
Build new features with Compose
Build a library of common UI components
Replace existing features with Compose one screen at a time
Remove Fragments and Navigation component and migrate to Navigation Compose
Since we won’t be adding new features to Sunflower, to complete the migration of Sunflower we will be focusing on steps 2–4. Specifically, we will be migrating the contents of each screen in the app to Compose while creating common reusable UI elements. When all screens are in Compose, we can then migrate to Navigation Compose and remove all Fragments (!!) from the app.
Note that this blog post builds on top of the Migrating to Jetpack Compose codelab so if you are new to migrating to Compose, I encourage you to check that out first. The codelab walks you through:
how to add Compose to an existing codebase
how to approach migrating existing UI elements to Compose one at a time
With our strategy in place, the next question is — which screen should we migrate first? Let’s take a look at Sunflower’s screens and navigation structure to help inform where we can get started.
The entry point to the app is HomeViewPagerFragment which is implemented as a view pager containing two pages/Fragments — GardenFragment and PlantListFragment. If we were to migrate HomeViewPagerFragment first, that would involve having to use Fragments within Compose. We would then later have to refactor our work once the contained Fragments are converted to Compose. To save us from this hassle, ideally each page should be migrated first before migrating HomeViewPagerFragment.
Given this structure, we’ll migrate all the other screens first (the order doesn’t matter), followed by migrating HomeViewPagerFragment last.
I’ll spare you the nitty gritty details of migrating each screen, though generally the migration process for each screen can be summarized by the following steps outlined below. Because Sunflower already followed our architecture best practices and guidances, migrating screens one at a time was isolated to the UI layer and we didn’t have to make changes to the data layer at all.
Create a screen-level composable (e.g. PlantListScreen).
Start migrating UI elements from the corresponding XML by using a “bottom up” approach (i.e. starting at the leaves of the UI tree and working your way up). For simple screens, you can do this all in one change/pull request. But for more complex screens, the bottom up approach allows you to make improvements in smaller increments which can be safer to do.
Identify if any components can be reused from previous screens. For example, both GalleryFragment and PlantListFragment have similar list item views but with different data types.
Once the screen has been created, update the implementation of the containing Fragment to return a ComposeView containing the newly created screen wrapped around a MdcTheme so Sunflower’s existing XML theming is applied to the screen.
In addition, for each screen-level composable:
Create a corresponding composable preview for the screen. This enables us to quickly iterate through the screen being built without having to deploy the entire app to an emulator or device.
For posterity, each migrated screen has a corresponding UI test that tests basic functionality for the screen.
To see this in action for each screen, refer to the linked pull requests below:
…and now, moving onto the last (and most satisfying) step of the migration process 🥁
Once all screens have been migrated to Compose, there’s very little benefit Fragments give you at that point. So as the last step for the migration process, we can delete Fragments, their associated XML files, and any related dependencies from the app, and use Navigation Compose to route between each screen.
The new navigation graph in Compose looks like this:
The pull request for this change can be seen here.
Note that Fragment and associated resource deletion doesn’t necessarily need to happen as the last step. In fact, both GardenFragment and PlantListFragment were deleted when HomeViewPagerFragment was migrated to Compose since both Fragments were used within a ViewPager2 and were not a part of the Navigation Component graph.
Migrating Sunflower to Compose was not without its own challenges. These weren’t necessarily roadblocks that prevented Compose adoption, but rather issues to be taken into consideration during migration.
As of this writing, Sunflower is built using Material 2, and to implement the collapsing toolbar behavior requires either a manual implementation of it, or using ComposeView held within the View-based CoordinatorLayout. You can then communicate the nested scroll state back to CoordinatorLayout via Modifier.nestedScroll and rememberNestedScrollInteropConnection(). This is precisely what was done in HomeViewPagerFragment (see HomeScreen.kt for a code sample).
A counterpoint to this, however, is that collapsing toolbars are well supported in Compose with Material 3 — the next generation of Material Design. Migrating Sunflower to Material 3 would avoid this issue altogether.
In Compose, state controls the UI so stateful drawables like StateListDrawable are inherently incompatible with how Compose works. The workaround for this is to use a single VectorDrawable (one of the states in the StateListDrawables) and rely on tinting the drawable for different states (see HomeScreen.kt for a code sample).
The last step of the migration process, which is to migrate to Navigation Compose, was fairly simple to do with Sunflower. This is due to the fact that Sunflower has only 5 screens to work with and so the migration process was relatively easy. However, for apps that have a lot more screens, the inability to migrate incrementally to Navigation Compose can be challenging as that change can be quite large to do all at once.
There is an existing feature request to improve upon this to enable an incremental migration so that Navigation Compose can be introduced earlier in the migration process. If this affects you, you can follow the issue here: https://issuetracker.google.com/issues/265480755
Overall, the migration process to Compose was smooth. It was very satisfying removing a lot of code and overall simplifying the implementation which helps with future maintenance on the app.
What did you think? Let me know in the comments below if you have any questions or have learnings to share in your journey migrating an app to Compose. If you want a more hands-on and guided experience with migrating to Compose, make sure to check out our codelab.
The migration journey is also documented on GitHub in this doc.