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.
Welcome back to our Compose migration journey 🚢 ! In part 1️⃣, we covered:
- 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
Let’s continue onto new adventures!
Step 0: Migration prep [in part 1]
Step 1: Dependencies and theming [in part 1]
Step 2: Smallest common UI components [in part 1]
Step 3: Migration of more complex components [in this post]
Step 4: Migration of low risk screens [in this post]
Step 5: Migration of more complex screens [in this post]
Step 3: Migration of more complex components
🗒️ Tasks:
- Find complex elements
- Migrate elements
Find complex elements
One of the biggest advantages of Compose, in my totally unbiased opinion 🙄, are Lazy layouts and their simplicity compared to RecyclerView
. Therefore, this step rightly aims to remove as much RecyclerView
-related code as possible and replace it with Compose. For a detailed step-by-step doc, refer to Migrate RecyclerView to Lazy list migration guide.
Our first target is the email attachment grid.
Migrate elements
We start by replacing EmailFragment’
sRecyclerView
XML with a ComposeView
, keeping its positioning constraints as is:
All other attributes, like paddings, etc., will be defined in its composable, as we want to make it customizable. We then find where the previous RecyclerView
binding was used, in the EmailFragment
, and add:
That’s all it takes. No ViewHolder
s, no Adapter
s — a vast amount of code removed by migrating just one component.
The previous EmailAttachmentGridAdapter
had complex, custom logic to simulate a staggered layout, which we’ve simply replaced with a LazyVerticalStaggeredGrid
. This API has a very cool way of defining a dynamic and adaptive size of your grid, by setting a minimum size for an item and letting the grid fit as many columns or rows as it can in the given space. This is perfect for supporting different form factors and a great adaptive user experience:
In the PR, we also replace another RecyclerView
with EmailAttachmentRow
, but we will ⏩ that.
Now enjoy removing entire classes such as EmailAttachmentAdapter
and EmailAttachmentViewHolder
and other layouts 😈.
💻 PR: Step 3
🏅 MVP player: Arrangement
API in Lazy layouts. Its capabilities make the customization of lists and grids in Compose even easier. For more visuals, check out the MAD Skills episode — Fundamentals of Compose Layouts and Modifiers.
💎 Pro tip: Instead of a grid, you could also use a Flow layout, depending on your designs. Or write your own custom layout. Up to you. The beauty of Compose is that you can easily achieve one design in multiple different ways.
Step 4: Migration of low risk screens
🗒️ Tasks:
- Find the best screen candidates
- Migrate screens
Find the best screen candidates
Now, almost all fragments have a bit of Compose code, and we’re able to move onto screen-level migration.
Remember that you don’t need to migrate old code to Compose — you can just use it for all new features and screens onward. However, if you are able to fully migrate, you should know it brings additional benefits:
- Less UI code and files to dig through
- Maintenance of a singular, declarative UI framework
- Avoiding context switching between Views and Compose
- Simpler and easier APIs to write and use, like animations and Lazy layouts
- Simplifying testing and navigation by using Compose-only, avoiding interop
Since Reply isn’t getting any new features anytime soon, we choose to refactor existing screens.
On screen-level, there are different tiers of form factor support. Some require major design changes, like implementing list-detail or other canonical layouts, while some require a basic support, like not being letterboxed.
Target screens that don’t require major layout changes and are low-risk to migrate first to Compose.
Migrate screens
Search screen was the perfect candidate, as it is a low-engagement, low-complexity feature.
Since all individual items were already converted, migrating this screen was a piece of cake 🍰:
At screen-level, we add device multipreviews to ensure our composables look great on all screen sizes:
SearchFragment
UI now consists of a SearchScreen
composable. We keep the fragment only as an outer wrapper to continue using the existing navigation framework and transitions, following the principles of the Navigation migration guide, but we can remove any old XML files and resources.
💻 PR: Step 4
🏅 MVP player: Window insets and corresponding modifiers like systemBarsPadding
. These are important at screen-level, to ensure your app and the system UI collaborate well and don’t get in each other’s way:
💎 Pro tip: If there’s a View component too complicated to migrate yet, and is blocking you, wrap it in an AndroidView
and keep using it in Compose. Make it a problem for your future self 😁.
Step 5: Migration of more complex screens
🗒️ Tasks:
- Find the best screen candidates
- Design for form factors at screen-level
- Migrate screens individually
- Pair into list-detail canonical layout
Find the best screen candidates
Our previous step was looking at low-risk/low-engagement screens, with minimal to no layout changes required for supporting form factors.
But in every migration journey, there will come a time for those riskier and more complex screens. Fear not, there’s a safe way of doing this.
This is a great time to involve your designer and rethink some screens with a form factor mindset. The functionality and UI of your classical phone display doesn’t need to change, and you’ll be able to leverage the power of Compose to build reusable, adjustable composables and combine them differently based on the screen size.
Canonical layouts play a major role in providing the best user experience, as they consider common use cases for how apps adapt visually to more or less space. Analyze your app and flag screens which could be a good fit for Feed, List-detail, or Supporting pane.
In Reply, the canonical use cases are Home and Email fragments, as they fit the list-detail layout perfectly:
Design for form factors at screen-level
To set the right mindset, use this form factor cheatsheet for screen-level composables:
- Start with low-risk screens that require least design changes, and scale to high-risk, more complex screens
- Make decisions based on the space that is allocated to your screens, and not fixed, hardware values
- When given an amount of screen space, think about how you can best use it to display content to the user. Could you use canonical layouts or realign some elements differently, based on the screen size?
- Ensure the app and screens are consistent during configuration changes like device rotation, by maintaining the scroll position, a logical navigation destination, and relevant visible information
- Use
WindowSize
classes to define different content types and turn them into observable state - Pass this state only to screen-level composables. For lower-level, you shouldn’t use the current window metrics directly
- Rely on adaptive tools such as: Grid’s adaptive and span APIs, Window insets, canonical layouts, Flow layouts, previews and multipreviews for all screen sizes, resizable emulator, scrollable modifiers,
fillMax…
modifiers,weight
s, adaptiveArrangement
s for lists and grids, and Accompanist adaptive utils
Migrate screens individually
We start by migrating Home and Email fragment’s UI to Compose HomeScreen
and EmailScreen
, and then pair them up in a Home and Email list-detail. Writing composable replicas of EmailFragment
and HomeFragment
is pretty straightforward, so we will ⏩ that.
We use Window Size classes to help convert the raw screen sizes into meaningful size classes and group them into buckets:
This information should only be passed down to screen-level composables. For lower levels, we shouldn’t use the current window metrics directly or through CompositionLocal
, but rather use the space that the composable is given to render itself.
We pass the size class down as observable state to the “host” of the list-detail layout, which is the HomeFragment
and its corresponding HomeScreen
composable UI:
Pair into list-detail canonical layout
Whenever the window size class is Expanded
and the content type is TWO_PANE
, we want the Home screen host to show the Email list and Email detail in list-detail canonical layout:
Or else, we want the Email list and Email screens separate in their own fragments, as it was originally:
Here, we use a handy shortcut to quickly set up a list-detail layout and provide the strategy on how the two panes should be laid out: the TwoPane layout from Accompanist. However, you could also write your own list-detail, following the JetNews sample or the canonical layouts repository.
To ensure the list-detail looks good, we again use the multipreview for a sweet combo of theme and device previews:
To provide the best user experience, we want to keep the same scroll position of our Email list when switching between single and two pane (for example, when rotating a tablet device):
Since HomeScreen
decides which mode to display, we hoist a singular scrollState
and share it between the Email list alone and Email list in TwoPane
:
So, this was all HomeScreen
and its fragment so far — what about the EmailScreen
and its fragment?
At the start of this step, we migrated both HomeFragment
and EmailFragment
UI into Compose HomeScreen
and EmailScreen
, and kept the fragments as outer wrappers for navigation. HomeScreen
now hosts two use cases — Home with email list and Home with email list + email detail.
If the Home email list is in single pane and we tap on an email, we still need to handle navigating to EmailFragment
and showing EmailScreen
standalone, as before the migration:
In a Compose-only app, this would be handled by Navigation Compose as the best way to navigate between exclusively composable destinations. But in our interop sample, we’re keeping Jetpack Navigation and, therefore, require fragments as destinations.
The EmailScreen
detail needs to remain a separate EmailFragment
destination, so that it can be navigated to from either HomeFragment
or from any other point in the app.
Thankfully, we extracted the EmailScreen
composable, so we just share the UI between these two scenarios:
onEmailClick
in HomeScreen
is different depending on whether we want to perform an actual navigation action from Home to Email in single pane, or just update the already visible Email composable with new email data in TwoPane
(refer to previous images).
Note: At the time of writing this blog, Shared element transitions are still not possible, so we weren’t able to migrate element transitions.
💻 PR: Step 5
🏅 MVP player: NestedScrollConnection
. Reply has a View BottomAppBar
set in the activity_main.xml
which “surrounds” the entire app and all of our nested composable screens, and collapses on scroll:
Now that Home UI is in Compose, how do we maintain the collapsible interop between the View BottomAppBar
and the HomeScreen
composable? It’s actually so simple it looks like magic.
Just add rememberNestedScrollInteropConnection
to the scrollable composable and it connects everything behind the scenes:
💎 Pro tip: DisplayFeature
provides a description of a physical feature on the display. For example, the FoldingFeature
describes a fold in the flexible display or a hinge between two physical display panels. It works similarly to WindowSize
classes and is provided to TwoPane
:
And with this, we conclude our migration process!
- Migrate safely and incrementally — the code you feel comfortable (or just slightly uncomfortable!) migrating. Remember, your app doesn’t have to be fully 100% Compose to reap its benefits
- Not everyone needs to be a Compose expert to start writing Compose code. You can learn it on the go, while writing and reviewing. The beauty of Compose is that you can achieve one design in multiple ways, all potentially equally “correct”. There’s an extensive set of materials to start your learning process, but in the meantime the best way of learning is by doing!
- Have fun! I often forget this myself. You get caught up in deadlines, design requirements and a growing pile of Jira tickets, and you forget that we’re in Android development because (presumably) we love making apps. Having fun while doing so, even if it’s part of our work, should be mandatory! Compose contributes a lot to this fun experience.
- Last but not least, enjoy removing numerous XMLs, resources and massive lines of View code 😈
Once you get more comfortable with Compose, migrating screens and components becomes faster, fun and rewarding, reducing the amount of code and improving readability.
But it can be a daunting task for larger codebases. That’s why refactoring old code is not a necessity and you can use Compose only for building new features and screens.
To enable this, consider migrating common UI and design systems first, but make an informed decision on whether to keep the View components in parallel or not.
Additionally, to ease the migration workload, you can keep using your original navigation framework and only think about migrating to Navigation Compose once all your screens are in Compose.
If you’re redesigning screens or features, consider taking this opportunity to adopt Compose, as you will have to do some rewriting anyways. And this opens up more possibilities of introducing form factor support along the way.
Stay tuned for the sequel!
Migrating to Jetpack Compose — an interop love story [part 1]
Migrating to Jetpack Compose — an interop love story [part 2]