Migrating an XML Project to Jetpack Compose

Migrating an XML Project to Jetpack Compose
AUTHOR
Ondřej Bartoněk

At the beginning of this year, we started rewriting one of our Android applications into Jetpack Compose. Why are we doing this? Are we just creating unnecessary work for ourselves? Are we trying to stay trendy in the world of programming? Or perhaps, at the very least, when your partner mentions buying new shoes, we want to have the option to respond with, "Sweetie, that's lovely, but I just updated the paging library and tried out a new implementation of maps in Compose."

These may not be the exact reasons, although there's some truth to them. In this article, I'd like to delve deeper into the situation of rewriting an application into a new UI framework and explore this issue from various angles using an example from one of our applications where we are currently tackling the rewrite.

The “Why”

As developers, we are well aware of the dynamic environment in which we operate. However, this might not be so apparent to those who manage projects. One of our tasks (especially for the more senior members of our team) is to regularly, patiently, and gradually educate our clients about this reality. This is not only in the interest of the project's longevity but also for our mental well-being and, consequently, the speed of development.

New technologies often emerge much faster than trends in fashion. This, in itself, might not be so critical, but unfortunately, older technologies (those older than the brand-new ones) quickly become unmaintained and outdated by their creators. Not to mention compatibility with other libraries and security risks. Developers are thus under immense pressure to balance the development of new features and the effort to update the libraries they've already used, so as not to accumulate too much technical debt.

For those of you who have read Uncle Bob's "Clean Code," you will surely remember the technical debt curve. For those who haven't read the book, in a nutshell: as technical debt grows, the costs of change increase exponentially. So, rewriting XML files into Compose is simply another step in eliminating technical debt. However, as we will show, it's not entirely trivial, and there are many reasons for this.

What We're Working With

In our case, we're dealing with a moderately to highly complex mobile application with many interesting features (paging lists, calls to payment gateways, calls to bank identities, calls to other applications, deep links), API endpoints, and quite customised UI elements.

It is important to realise that rewriting the entire application is not (and will not be) a one-time effort but a long journey. Due to limited resources, gradual rewriting is the only acceptable solution for both the client and the developers. And of course, the right moment must be awaited.

"Step by Step" Execution

Internally, we decided in advance that in the first phase, we would write completely new features in Compose and leave the old ones as they are. At the beginning (before the initial excitement about Compose settles down), we will clean up "baggage" utility methods and delete what can be removed within the context of contextual changes. Only after that will we consider the actual extraction and rewriting of older code into Compose.

Once we received the green light to create a brand-new feature, we could start:

  1. Since our application was essentially a monolith not long ago, we took advantage of this situation and simultaneously created library submodules for the new UI approach: clean Compose components, as well as color and font definitions, etc.; for the old "XML UI" - View extensions, listeners, etc.; and for anything else that could be moved (a shared submodule primarily for resources and drawables). In total, we now have three new submodules in our library for UI purposes.
  2. Following agreed-upon architectural approaches, we created a feature module and, once again, a submodule for the new feature.
  3. We configured Gradle, creating custom plugins, for example, to share common configuration settings (e.g., buildFeatures.compose = true, namespace, and SDK configuration) within feature and library modules.
  4. We addressed navigation to new Compose features, which has become one of the largest compromises in the entire rewrite. In the end, we decided to keep it as it is (mainly due to concerns about existing functionality). However, this meant that new Compose screens must be wrapped in a Fragment and androidx.compose.runtime.Composable. Fortunately, this worked without any issues. However, it is only a temporary solution that we will want to eliminate in the future.
  5. Creating Compose components in a dedicated module is the cherry on top, and from this point on, it's our daily joy.

Further Compromises

Navigation to new Compose features was not the only temporary compromise we had to accept. Here are a few other specific examples we had to learn to live with:

  1. Wrapping so-called BottomSheet dialogs - in Compose, we call a Fragment(androidx.appcompat.app.AppCompatDialogFragment), which then calls Compose again. The reason for this is the requirement for the dialog to cover the bottom navigation toolbar, and at that time, ModalBottomSheet from androidx.compose.material3 was not available.
  2. Debugging Compose maps (com.google.maps.android.compose.GoogleMap) only in the release buildType - after several hours of trying to figure out why map scroll animations on the device were rendering like a photo album, we discovered that we either had to turn off StrictMode on the device or switch to the aforementioned release buildType.
  3. Rabbit holes in the form of refactoring utility methods - when we decide that part of the changes will include a minor extraction and cleanup of the monolithic module (due to some utility we want to use in the Compose module), we never know in advance how deep this will go.

Conclusion

There probably isn't a one-size-fits-all recipe for approaching the rewrite of an application into an entirely new UI framework. Since programming is a creative activity, each application may be written differently, in different architectures, in different programming languages, and may use different third-party libraries that provide the same functionality. Conversely, even slight variations in identical libraries are enough to make us look at the rewrite differently.

So, what should you take away from this article?
  • Create a rewrite plan that reflects the reality of your project (client's approach to technical debt, resource constraints, project lifespan, etc.).
  • If you don't have the time allocation for a complete codebase rewrite, come to terms with the fact that some UI elements/variables may be duplicated (even if temporarily).
  • Document, document, and document again - and keep the documentation up-to-date. Why is something done one way and something else another way? What should we do if we want to...? (Note: We solved this problem with JIRA tickets to track what is/isn't done. We also updated README and CONTRIBUTING files directly in the codebase to make it easier for us to adhere to established standards).
  • Accept that the rewrite will take a long time, and some old things may never be rewritten.

In short, rewriting is not an easy task and usually falls far from the ideal scenario. Nevertheless, I hope that similar escapades in your days are indeed happening because it means that your codebase is healthy, and your client understands and supports your efforts to maintain it in good shape.

And that, in my opinion, is good business.

Read Next

Let’s make something great

Do you need to ideate, design or develop an app?
We are here to help you.
Schedule a call