Blog

Our Journey from XML to Jetpack Compose UI

One of the key product offerings at mx51 is our in-store capability that connects a merchant’s POS to their payment terminal. To make this integration work, we have software that runs on both pieces of hardware. This post is about our Android app (known as our MXA – Merchant eXperience App) that runs on the payment terminal, and how we migrated it from using XML notation for screen layouts to Jetpack Compose UI. If you’ve ever used a payment terminal, our MXA would be the part where you enter an amount to pay or where you’d add a tip or cashout amount. It has lots of other screens too, menus to change the behaviour of the terminal and screens that allow you to securely connect to your in-store POS.

MXA Idle Screen MXA Purchase Screen MXA Support Menu
MXA Idle Screen MXA Purchase Screen MXA Support Menu

The Technology

Android apps since the dawn of time have used XML notation to describe their screen layouts, screens are made up of Views such as ConstraintLayout, LinearLayout, Button, EditText, etc. In May 2019, Google announced Jetpack Compose, a new UI framework and compiler for building screens and screen components in a declarative way using Kotlin code.

Jetpack Compose UI promised to make developing screens and custom components easier and quicker than with traditional views, with the added benefit of being easier to test, while keeping great features like IDE previews. All the signs from Google were hinting that this was the future direction for UI development on Android so we were keen to try it out at mx51.

Our Journey

Our journey to Compose UI started in September 2020 with the arrival of the first alpha releases. Earlier Developer Preview releases had been very volatile with breaking changes being made almost on a fortnightly basis. We knew this could still happen with alpha builds too, but the chatter on various Slack channels and forums suggested that a lot of the volatility had been ironed out… It was time for us to try it out!

Our MXA at this time was still under construction and was a long way from having its first v1.0 build, we had several screens already created using XML notation, but still many screens left to build. It therefore made sense for us to take a serious look at Compose UI, given that we’d rather not build everything in XML, only to then rewrite everything again in Compose UI.

The great thing about Compose UI is that Google provided a way for you to use it inside an existing Activity or Fragment without too much trouble. This meant instead of inflating an R.layout.keypad_screen, we could swap this out for the setContent { } call that would provide a bridge between the host fragment and our new composables. This was fantastic as it meant we could migrate a single screen at a time without breaking other parts of the MXA that we knew were already working.

We picked our simplest screen in the app (the Support Menu screen) and began the conversion. The initial setup of Compose was tricky to get right, the sample apps from Google helped a lot here. There were several compiler flags that needed setting, and Compose needed very specific versions of Kotlin to work. As with any new library, you might spend a lot longer getting the first screen to work, but the second and third screens are a lot quicker. We found this to be true on our project, as the initial setup of the Material theme, colors, shapes and fonts took time to get right.

MXA Support Menu
MXA Support Menu - the simplest screen in the app

As we migrated more and more screens we found ourselves creating more and more reusable components, something that we previously weren’t doing in the XML layouts. In the past, creating include_fancy_widget.xml and then inserting it into other views was a bit clunky, because you usually had issues deciding where the padding lives, or do we need a <merge> tag at the top level to reduce the hierarchy depth? I found it hard to implement custom views in code too, making sure the layout and measure passes were done properly for a complex view was never easy.

With Compose UI these worries seem less of a problem. Hierarchy depth is a concern of the past now, you can (within reason) have as many Row or Column composables nested as deeply as you want, without suffering any performance impacts from repeated layout and measure passes.

The first few composables we created were for simple things like PrimaryButton, SecondaryButton and MenuButton, but we quickly moved on to other more complex composables, which we named scaffolds borrowing the name from existing components in Compose UI. These scaffolds provided the overall layout template for our 4 main screen designs: we had a MenuScaffold for the screens that have lists of buttons to other screens (see screenshot above), the KeypadScaffold was used to build any screen that needed a keypad, and so on. This worked really well for us as it meant that building most of our app consisted of grabbing the right scaffold composable and then adding in the unique missing part of the screen.

This code shows our early implementation of the MenuScaffold, it defines the toolbar and background for all our menu screens and automatically makes our list of buttons scrollable (in case there are more than will fit on a single screen). We extended the MaterialTheme to add some custom fields and named this MXATheme.

@Composable
fun MenuScaffold(
    @StringRes title: Int,
    btnBackClicked: (() -> Unit)?,
    btnCloseClicked: (() -> Unit)?,
    content: @Composable (ColumnScope.() -> Unit)
) {
    Scaffold(
        topBar = {
            MenuAppBar(
                title,
                btnBackClicked,
                btnCloseClicked
            )
        },
        backgroundColor = MXATheme.colors.menuToolbarBackground
    ) {
        Surface(
            color = MXATheme.colors.menuBackground,
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
        ) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .verticalScroll(
                        rememberScrollState()
                    )
            ) {
                content()
            }
        }
    }
}

The Support Menu screen then simply specifies the buttons that are contained within the menu.

MenuScaffold(
    string.settings_title_support,
    btnBackClicked,
    null
) {
    MenuButton(
        title = string.settings_support_btn_contact_us,
        icon = drawable.ic_btn_contact_us,
        btnClicked = btnContactUsClicked
    )
    MenuButton(
        title = string.settings_support_btn_about_this_terminal,
        icon = drawable.ic_btn_about_this_terminal,
        btnClicked = btnAboutTerminalClicked
    )
    MenuButton(
        title = string.settings_support_btn_network_test,
        icon = drawable.ic_btn_network_test,
        btnClicked = btnNetworkTestClicked
    )
    MenuButton(
        title = string.settings_support_change_passcode,
        icon = drawable.ic_btn_change_passcode,
        btnClicked = btnChangePasscodeClicked
    )
    MenuButton(
        title = string.settings_support_btn_restart_device,
        icon = drawable.ic_btn_restart_device,
        btnClicked = btnRestartDeviceClicked
    )
}

In this example you can also see our reusable MenuButton composable, which takes a title, icon and callback lambda as parameters. The MenuButton contains all the styling, padding and layout of those components.

@Composable
fun MenuButton(
    @StringRes title: Int,
    icon: Int,
    isEnabled: Boolean = true,
    btnClicked: () -> Unit
) {
    Button(
        onClick = {
            if (isEnabled) {
                btnClicked()
            }
        },
        modifier = Modifier
            .height(86.dp)
            .padding(20.dp, 0.dp, 20.dp, 16.dp),
        enabled = isEnabled,
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MXATheme.colors.menuButtonBackground,
            disabledBackgroundColor = MXATheme.colors.menuButtonBackground.disabled()
        ),
        shape = MXATheme.shapes.menuButton
    ) {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Start,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                painterResource(id = icon),
                contentDescription = null,
                tint = MXATheme.colors.menuButtonText
            )
            Spacer(modifier = Modifier.width(16.dp))
            Text(
                stringResource(id = title),
                color = if (isEnabled) MXATheme.colors.menuButtonText else MXATheme.colors.menuButtonText.disabled(),
                fontWeight = FontWeight.W500,
                fontSize = 18.sp
            )
        }
    }
}

We spent about 3 months converting the existing screens within the app from XML to Compose UI, although this process wasn’t 100% of our time (there are always meetings, etc). But once the conversion of existing screens was done, we had a great collection of reusable composables that made building the remaining new screens very quick indeed. The beauty of this reuse was highlighted when we came to branding our MXA for different clients. The screenshots you see above are for our demo version of the app called “Gecko Bank MXA”. However, we need to support color schemes and fonts for lots of different customers, using build variants in Android Studio we were able to extract the definitions of these brands into different build flavors, which meant we could easily build different branded versions of the app.

The MXATheme acts as a proxy to the real light and dark colors for a brand, it’s easy for us to see if a composable is using a hard-coded hex color definition during a PR review (something we could even write a lint check for). This means we can enforce the use of the MXATheme throughout the app and therefore easily manage adding new brands to our codebase. Combine this with the IDE previews that Compose supports and it’s very easy to see if there is white text defined on a white background before you even need to build and run the app.

Our MXA was released into production on July 2021 and until recently we continued to build all new screens using a single activity and multiple fragments with Compose UI inside them.

The Future

Our next release will contain changes to convert all our navigation framework from the Google navigation library to the navigation-compose library. What this means is that we’ve now removed a whole layer from our technology stack (bye bye, fragments!). Our single activity hosts our navigation graph, which in turn drives which composibles are displayed based on navigation routes.

This is great as it means we rely even less on some Android specific components, allowing us to consider building our app for other target platforms, e.g. the desktop.

What Did We Learn?

Alpha Releases Were Pretty Volatile… Of Course!

The alpha releases caused us some headaches as the community behind Compose UI kept providing more feedback to Google. This changed how it worked and what components were provided out of the box. They were certainly less volatile than the Developer Previews, but after converting our first few screens we enjoyed using Compose UI so much more than the old XML way that we only wanted to commit to Compose UI further.

We knew we had many screens left to build in our MXA and that using Compose UI for all of it would be a good move in the long term. So the headaches we faced as we moved through the alpha/beta releases was worth it for us. Jetpack Compose 1.0 went stable in July 2021, so the challenge of handling the breaking changes should be less of an issue if you’re starting a similar journey today.

Double Themes

While migrating from XML to Compose UI, you’ll find that you have your app theme defined in 2 places (1 for XML and 1 for Compose UI). This can be a little tricky to manage, especially for a new app where you may still be defining colors, typography, etc. Thankfully, to make things easier Google provides a Theme Adapter, which allows you to bridge your theme definition from XML over to Compose UI. We didn’t use this in our implementation since we knew that our XML would soon be disappearing altogether, we just stopped updating anything relating to XML and focused on Compose UI.

Compose UI Is Easy to Try

We were able to try out Compose UI on a pretty simple set of screen designs to see if we liked it, which we did. Our architecture at the time also made it easy for us to convert one fragment at a time over to Compose UI. The developers and QA team could easily check if the migrated screens worked as intended in an isolated way, which gave us confidence that we hadn’t gone backwards and introduced bugs in things that were already working.

If you have a large app that you’re considering moving over to Compose UI, this method is a great way to try it out in a small part of your app first.

Compose UI Is Easy to Learn

Our team is a mix of developers with different levels of Android experience. We found that everyone got up to speed quickly on how Compose UI worked, and everyone was creating custom components with ease (something that I personally found hard to do when using custom views in the old view framework).

For developers who might not have a strong Android background, Compose UI is also easy to learn. It’s one fewer syntax to learn, since everything is in Kotlin, there is no need to wrap your brain around XML tags and attributes.

Google provides a great set of Jetpack Compose Samples to help you learn the ins and outs of Compose UI, and many Android frameworks are now supporting Compose UI, e.g. COIL for Image Loading, Accompanist for inset management, permissions, navigation animations and much more. So you have all the tools you need to build great apps.

Epilogue

Overall, we’re very happy with the decision to move all our MXA code over to using Jetpack Compose for the UI and navigation within the app. I personally think we made the choice to migrate at the right time, and we were very fortunate that v1.0 of Compose UI went stable about a week before our first pilot release of the MXA. Jetpack Compose is now a lot easier to dive into as well, the numerous compiler flags we needed in the beginning are now gone. You still need a fairly specific version of Kotlin for a given Compose version, but you can see this is getting more flexible over time too.

I highly recommend anyone considering Compose UI to try it out as soon as possible, it’s a joy to use, it boosts your productivity and will be a big part of the future of Android development.