How we switched from XML to Jetpack Compose at Süddeutsche Zeitung

Michael Bichlmeier
Süddeutsche Zeitung Digitale Medien
10 min readMar 1, 2024

--

As part of the Android team at Süddeutsche Zeitung Digitale Medien, I was able to help drive forward an exciting and challenging development. In 2022, we decided to switch our SZ app completely to Jetpack Compose. As Compose was only available in beta at the time, it was difficult to tell if it was even production ready.

In the following chapters I will show you how we minimized potential risks that came along with the migration and how we managed and structured it inside our team.

We were faced with the need to adapt our way of thinking — moving away from a static layout approach to a more dynamic, fluid model of user interface design. The hoped-for improvements were diverse and attractive: an increase in developer productivity through less boilerplate code, improved app maintainability and a richer and responsive user experience that meets modern standards. We were keen to improve the interactivity of our app, make animations and transitions more seamless and achieve smoother performance overall. There was a lot of excitement in the team because the benefits Jetpack Compose promised could allow us to not only improve the technical quality of our work, but also take the user experience of our readership to a new level.

Imperative UI vs. declarative UI

In the past, when we still relied heavily on the imperative approach, UI development for us Android developers was a constant juggling act with the state of the UI elements. Especially in large and lively apps, this could become quite complex and prone to errors. With XML layouts that we connected with ViewBindings or findViewById(), we had to draw a clear line between the look of our UI and the logic behind it — this was often difficult and not particularly flexible.

Jetpack Compose has changed the game for us. It takes the burden off us by allowing us to describe our UIs declaratively. We define how the UI should look in different states, and the framework takes care of the updates in the background. The result? Our code is now cleaner, we can focus more on design, and the error rate has decreased significantly.

XML vs. Jetpack Compose in practice

Here is a simple layout in XML compared with an approximately same layout in Compose. There is a huge difference in the readability of the code as well as in the flexibility.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/shared_spacing4"
android:paddingBottom="@dimen/shared_spacing4">

<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline_start"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/shared_content_start_percent" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline_end"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/shared_content_end_percent" />

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toTopOf="@id/title_text_view"
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_end"
app:layout_constraintStart_toEndOf="@id/vertical_guideline_start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:srcCompat="@drawable/shared_ic_unicorn" />

<TextView
android:id="@+id/title_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/shared_spacing7"
android:gravity="center_horizontal"
android:textAppearance="?oneTextAppearanceHeadline3"
android:textColor="?oneColorHeadline1"
app:layout_constraintBottom_toTopOf="@id/description_text_view"
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_end"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/vertical_guideline_start"
app:layout_constraintTop_toBottomOf="@id/icon_image_view"
tools:text="@tools:sample/lorem[10]" />

<TextView
android:id="@+id/description_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/shared_spacing7"
android:gravity="center_horizontal"
android:lineSpacingExtra="@dimen/shared_spacing2"
android:textAppearance="?oneTextAppearanceCopy1"
android:textColor="?oneColorCopy1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_end"
app:layout_constraintStart_toEndOf="@id/vertical_guideline_start"
app:layout_constraintTop_toBottomOf="@id/title_text_view"
tools:text="@tools:sample/lorem[10]" />

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

It’s nearly impossible in XML to define an if-else statement for the UI elements. This has to be done and managed in the fragments themselves. Compared to Jetpack Compose you have all possibilities to do your UI logic directly inside your UI definition.

@Composable
fun CustomLayoutScreen() {
val scrollState = rememberScrollState()

Box(modifier = Modifier.fillMaxHeight().fillMaxWidth(), contentAlignment = Alignment.Center) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxWidth(0.9f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.shared_ic_unicorn),
contentDescription = null,
modifier = Modifier.size(24.dp),
alignment = Alignment.Center
)

Text(
text = stringResource(de.swmh.szapp.R.string.lorem_ipsum),
modifier = Modifier
.padding(top = OneTheme.dimensions.space7)
.fillMaxWidth(),
style = OneTheme.typography.headline3,
textAlign = TextAlign.Center,
color = OneTheme.colors.headline1
)

Text(
text = stringResource(de.swmh.szapp.R.string.lorem_ipsum),
style = OneTheme.typography.copy1,
modifier = Modifier
.padding(top = OneTheme.dimensions.space7)
.fillMaxWidth(),
textAlign = TextAlign.Center,
color = OneTheme.colors.copy1
)
}
}
}

Composition instead of inheritance

Jetpack Compose is all about the principle of composition, a real game changer for our team. Instead of struggling with inheritance to create and customize UI components, we now use the power of composition through reusable functions that are called composables. We can combine and reuse these flexibly. This not only makes our code more reusable, but also simplifies testing enormously.

Advantages of Jetpack Compose

Since switching from XML to Jetpack Compose, we’ve seen some clear benefits that make our developer lives easier:

  • Increased developer productivity: Thanks to the declarative syntax, we say goodbye to boilerplate code and speed up our development process. It feels like we’re flying instead of walking.
  • Interoperability: One of the coolest aspects of Compose is that it works seamlessly with our existing codebase and the old XML layouts. This means we can migrate incrementally without having to rebuild everything at once — a true blessing
  • Intuitive and powerful layouts: Compose comes with a lot of intuitive layouts and UI components that bring features like flexible composition, animations and gestures right out of the box. Previously, we had to write extra code and add libraries for such features. Now it’s all directly available.
  • Live Preview and Hot Reload: Having direct feedback when we work on the UI code without having to restart the app has taken our ability to experiment and iterate to a new level. It not only saves time, but also makes development much more reactive and fun.

Disadvantages of Jetpack Compose

As you can probably imagine, everything has its drawbacks…

  • Performance Considerations: Jetpack Compose is designed to be efficient, but like any framework, there can be performance implications if not used correctly. Especially in complex UIs, getting performance right might require deeper understanding and sometimes more work compared to the traditional approach. The performance of your debug and release builds also differs significantly.
  • Limited Third-Party Libraries Support: While the ecosystem is rapidly growing, some third-party libraries may not yet support Jetpack Compose, or their Compose counterparts might not be as mature. This could mean additional work to create Composable versions of existing components or waiting for the ecosystem to catch up.
  • Tooling and Debugging: Although Android Studio provides support for Jetpack Compose, developers might find that tooling, especially around debugging and previewing Composables, is still evolving. This can sometimes make debugging more challenging compared to the traditional View system.

Preparing for the migration

Switching from the traditional Android XML layouts to Jetpack Compose was a big step for us at Süddeutsche Zeitung Digitale Medien. Of course, it required a lot of planning and preparation. The initiative came from the development team because we always want to stay up to date and try out the latest technologies. In this section, I’ll tell you how we approached the migration — from the initial discussions with our product owner to the strategic planning of the whole process.

Initiative and coordination

It all started with our own initiative. We didn’t just want to improve our app, we also wanted to bring it up to date with the latest technology. The first step was to talk to our product owner and make sure that our plans are really aligned with the company’s goals. We thoroughly discussed all the benefits that Jetpack Compose would bring, such as increased developer productivity and a more modern UI, but also considered the potential risks. These were exciting discussions that showed us all how important it is to be on the same page.

Risk assessment and joint decision-making

An important part of the preparation was to talk openly about risks and develop a plan on how to minimize them. We and our PO agreed that trying out new technologies is essential to stay competitive and improve the user experience. Of course, using a beta technology can be challenging, but after extensive discussions, we jointly decided to take the plunge and agreed on a step-by-step approach to minimize the risks.

Step-by-step approach

We decided to write only new components in Jetpack Compose for the time being. This was a smart move, as it allowed us to gain experience without disrupting the running system. We documented every newly written component in detail - so everything remains clear and to serve as an example in future rewrites.

Training and resources

Of course, we also had to familiarize ourselves with Jetpack Compose first. We used all the resources available, from the official documentation to online courses and community contributions. This learning phase was super important so that we could all feel confident using the new tool.

Summary

Preparing for the migration to Jetpack Compose was a real team effort. Thanks to close collaboration with our PO, a thorough risk assessment and the decision to take a step-by-step approach, we created a solid foundation for the transition. Training the team and accurately documenting our progress has allowed us to take full advantage of the many benefits of Jetpack Compose while minimizing the risks. It has been an exciting journey that has not only moved us forward technologically but has also brought our team even closer together as a team.

The migration process

Migrating our app from XML to Jetpack Compose was a real adventure. We had to think carefully about how to proceed, because it was clear that this migration had to be carefully planned and executed. Here I tell you how we mastered this process step by step.

Creating a migration plan

Together, we drew up a detailed plan that defined which components had to be migrated and when. We had to keep a close eye on the dependencies within the app and keep our plan flexible to be able to react to any surprises. It was a dynamic process that evolved with us. We also considered that we would have to develop some app features in addition to the migration.

Migration of simple layouts as a starting point

We started with the simpler layouts as a kind of warm-up. These first steps not only gave us a good feel for Jetpack Compose, but also quickly delivered visible results. Each migrated component was an opportunity to refine our approach and optimize the code for the future.

Step-by-step migration of more complex layouts and components

With our newly gained confidence , we tackled the more complex challenges. This involved not only more complicated layouts, but also integration with data and logic. It was a constant give and take, during which we learned a lot about the possibilities of Compose.

Integration with existing architecture patterns

An important milestone was the seamless integration of Compose into our existing architectural patterns, especially MVVM. It was crucial to integrate Compose perfectly into our system without compromising the existing functions. As we used Kotlin Flows from the beginning also with the XML Layouts, we were able to reuse the existing data flows from the ViewModels in the Composables without any problems (almost).

Navigation: Switching to ComposeDestinations

A real turning point was the decision to switch our entire navigation to ComposeDestinations. We found Jetpack Compose standard navigation strategy overly complex with its route definitions and its argument passing. That’s why we opted for a library that abstracts navigation and routing much better.

From our point of view, a step-by-step conversion of the navigation was not possible, as the coupling was too high with our code and modules and would only have created more chaos if we had to go down two tracks here. We therefore decided to replace the old navigation within one go.

In my opinion, that was the most complex part of the whole migration. All the complexities came together here. We had many dependencies to our modules that were broken from one moment to another and we had to replace and fix them with the new navigation logic step by step without knowing if everything will work at the end.

@RootNavGraph(start = true)
@Destination
@Composable
fun StartScreen(destinationsNavigator: DestinationsNavigator) {
Button(onClick = {
destinationsNavigator.navigate(
AnotherExampleScreenDestination(
anyString = "",
anyObj = ParcelableObject("name", 30)
)
)
}) {
Text(text = "Navigate to another screen")
}
}

@Destination
@Composable
fun AnotherExampleScreen(anyString: String, anyObj: ParcelableObject) {
Text(text = "$anyString: ${anyObj.name} and ${anyObj.age}")
}

@Parcelize
data class ParcelableObject(val name: String, val age: Int) : Parcelable

This example shows how clean and type safe we can now navigate with ComposeDestinations. Compose in its standard navigation offers the same possibility but with much more complexity and a higher risk of bugs and errors. For the compose navigation have a look on the Android Developer website.

Challenges with WebViews

WebViews were a challenge of their own as we use them mainly for advertising, embeds and our paywall (and some special articles). Since we didn’t find a fitting solution in Compose, we simply integrated our WebViews into AndroidViews. We had to manage the handling of recompositions and the loading of the WebViews cleverly to avoid performance issues. The solution was a WebViewStore that keeps the WebViews in memory and reuses them when needed on each recomposition.

Completion of the migration

After a year of hard work and constant improvements, it was done: our app was fully migrated to Jetpack Compose. It was a proud moment for all of us, underlining our commitment and adaptability to new technologies.

The migration was a challenge, but also an incredible opportunity to learn and grow. The experience we gained is invaluable to us as a team and will help us move forward with future technological shifts.

The big cleanup

During and after the migration we had to do a lot of cleanups. We removed all fragments and all XML layouts. Luckily, the logic from the fragments had already been transferred in advance during the migration but we had to check twice whether we had really implemented everything we had previously in terms of UI logic in our composables. We also had to remove the old navigation structure and all leftovers of it. After that our project looked maintainable and much more structured as before.

Conclusion

Looking back, I would say the migration to Jetpack Compose was one of the best decisions we could have made as a team. Not only was it a technical necessity, but it also brought us even closer together as a team. We worked side by side and overcame new challenges together every day.

Special thanks goes to our PO Felix, who trusted us completely during this migration. His willingness to take risks enabled us to break new ground and grow as a team. Equally indispensable was Swen and Severin, our testers, who made decisive contributions to our success with their courage and constant willingness. Without the tireless work and support of every single developer, both actively and in the background, this migration would not have been possible.

This experience has shown us what is possible when we all pull together and are open to new ideas. It was a journey that I would take again without hesitation.

--

--