Breaking Up with XML: Migrating Quotes app UI to Jetpack Compose

Mirzamehdi Karimov
4 min readMay 3, 2023

In this blog, I will share my experience of migrating the Quotes App UI to Jetpack Compose. I will provide step-by-step guidance that you or I can use for my ongoing projects :). However, my focus will be on the challenges I faced during the refactoring process, rather than technical details. For those, who would be more interested in technical part I recommend this Compose Migration guide codelab
(https://developer.android.com/codelabs/jetpack-compose-migration).

Pull Request of migration: https://github.com/mirzemehdi/quotesapp/pull/1

During the migration process, my primary goal was to focus on the UI migration and not make any changes to other parts of the project’s architecture. For example, if there were any issues related to how data is observed in the UI or how it changes, I would implement temporary solutions instead of changing the entire architecture. This approach is particularly useful for larger codebases as making changes to other parts of the codebase would be time-consuming and challenging.

Get ready to put your coding hats on and let’s make Jetpack Compose our new BFF! 😎💻🚀

1) Adding Jetpack Compose dependencies:

dependencies {

val composeBom = platform("androidx.compose:compose-bom:2023.03.00")
implementation(composeBom)
implementation("androidx.compose.ui:ui")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")

// Material Design 3
implementation("androidx.compose.material3:material3")
// or Material Design 2
implementation("androidx.compose.material:material")
// or skip Material Design and build directly on top of foundational components
implementation("androidx.compose.foundation:foundation")

// Optional - Integration with LiveData
implementation("androidx.compose.runtime:runtime-livedata")

//Lifecyle compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.5.1")

//Navigation compose
implementation("androidx.navigation:navigation-compose:2.5.3")

//If you are using Koin as DI, this should be also added to dependendencies
implementation("io.insert-koin:koin-androidx-compose:3.4.2")

}

2) Enabling Jetpack Compose in build.gradle file:

  • Increasing kotlin version to 1.8.10
  • Increasing compileSdk and targetVersion to 33
  • Enabling Jetpack Compose
buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.4.4"
}

3) Modifying Detekt config file for Jetpack Compose

Since I was using detekt for static code analysis, it was giving some warnings regarding Jetpack compose, such as FunctionNaming rule, which was giving warning about Composable function names as they start with a capital letter. In order to avoid these warnings, I added ignoreAnnotated:['Composable'] for the FunctionNaming, UnusedPrivateMember, LongParameterList, and LongMethod rules in Detekt's config file.

Detekt config file

4) Migrating Fragments to Jetpack Compose

I started migration with the Profile screen since it was just a static screen with no data. I added an empty ComposeView to the fragment layout xml file and gradually added views to this ComposeView one by one. Once all screens are complete, we can implement navigation between them. As an initial step, I just added the skeleton of each view, such as three components of the Profile screen: Image, Text, and one Text inside a box. I focused on sizing, padding, and placement of components on the screen and did not apply any styling, coloring, or fonts to them.

Profile Screen skeleton (without any styling)

The next step was to focus on theming, coloring, and applying fonts. I used custom theming instead of material theming as it is confusing for me. Also, custom theming allows me to add custom attributes easily specific to the project. You can check this Google Jetpack Compose sample to learn more about applying custom theming in your project
https://github.com/android/compose-samples/tree/main/Jetsnack.

In the common UI module, I have added fonts, colors, and theming so that they can be used in other modules as well.

Once styles have been applied to each component in the Profile screen, it is safe to delete the profile XML file. However, the Fragment still needs to be kept until all screens are migrated to Compose. Inside onCreateView method of the Fragment, we will return the Profile Compose View.

return ComposeView(requireContext()).apply{
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { ProfileScreen() }
}

I repeated these steps for the other screens as well. If I came across components that were used multiple times, I moved them to the components package inside the UI module. However, if they were only used in a feature module, I kept them inside that module.

In the current version of the project, I have used SingleEvent to handle one shot events, such as, navigation or showing toast messages. To handle this as a state without refactoring all instances of SingleEvent to state, I have added a temporary solution by adding this function which can be observed in jetpack compose as a single event.

@SuppressLint("ComposableNaming")
@Composable
inline fun <T> LiveData<SingleEvent<T>>.observeEvent(
crossinline onEventUnhandledContent: (T) -> Unit
) {

val singleEventValue by this.observeAsState()
LaunchedEffect(key1 = singleEventValue) {
singleEventValue?.getContentIfNotHandled()?.let(onEventUnhandledContent)
}
}

But for future I am planning to remove it and replacing it with a state as it is recommended way of doing. You can read this blog for more in-depth information about handling one shot events in Compose.

Once I finished migrating all screens, I implemented Compose Navigation. Honestly, even though the navigation part seemed simple, the most challenging part for me was implementing navigation even for this small application. This was because, in the current version of navigation compose, we need to completely migrate from the Navigation Graph XML to Compose. We cannot keep some parts in XML and some parts in Compose.

Finally, after all these processes, we can delete all fragments and fragment UI XML related files, and celebrate our success! Yaaaayy 🥳🥳🥳

Thank you for reading. Feel free to share your challenges as well while migrating to Compose.

--

--