How We Handled a Large-scale AndroidX Migration

Key takeaways from Gojek’s consumer app migration.

How We Handled a Large-scale AndroidX Migration

By Abhishek Birdawade

As developers, keeping ourselves updated with the latest trends in tech directly affects the experience we give our users. A year ago, Google launched the AndroidX framework, in which we saw tremendous improvements compared to the original Android Support Library. AndroidX ships separately from the Android OS, and provides backward-compatibility across Android releases. It fully replaces the Support Library by providing feature parity and new libraries.

This is the story of how we migrated the Gojek consumer app to AndroidX.

How Things Worked

First, let’s talk about our existing architecture. We have a mono repo called GoHost. It has a product-wise distribution in terms of modules. During the time of migration, our repository consisted of over 150 modules. So you can imagine the effort that went into this process.

A lot of things had to be considered because of our huge codebase:

  • Understanding all the possible problems, and doing dry-runs
  • Communicating to developers, managers, and all other stakeholders who would be affected due to the migration process
  • Taking measures to overcome the risk of instability of the consumer app
  • Deciding the approach of migration, either to stop the development or do the process in parallel.
  • Looking at areas that needed testing

The Dry Run

Initially, we tried this out with only one aim — getting it to successfully compile. This process ended up taking 4–5 working days due to compilation errors. Looking back at this phase, now I see how a lot of these steps could have been reduced and done better.

Introducing the X-Men ⨷

We had an Android developers meeting to showcase the first rough iteration on migration, in order to explain the efforts required. In this meeting, I covered intricate details — like how I tried migrating everything at once, what kind of compilation errors occurred, etc. After a lot of brainstorming, we decided to segregate the types of errors and perform a migration using a linear pattern:

  • Identify external libraries like Glide, Airbnb, etc. which support AndroidX.
  • Use the AndroidX migration script provided by Google, which will run over the project and replace Class and Artifact Mapping.
  • Use Gradle properties like useJetifer and useAndroidX booleans, and set them to true.
  • Solve compilation errors
  • Solve runtime errors
Looks pretty straightforward, right? Well, it wasn’t! 😐

We started planning with the Developer Experience Team. A few key points that were decided were:

  • Making this a two-pair job spread over four days
  • Point of contacts from each team to resolve all issues after merging everything to develop
  • Communication to the stakeholders and identifying blockers
  • Include Target SDK 28 as a part of this task

Here’s What the Roadmap Looked Like

Day-1 Update support library to 28.0.0, resolve all issues and make a successful build

Day-2 Run AndroidX migration script and make a successful build

Day-3 Update target SDK to 28, make successful build and test it

Day-4 Rebase all the changes which have been pushed on develop branch for the last 3 days, merge everything to develop branch and make a successful running build

Following this, the focus would shift to quality analysis and bug fixing, and monitoring the stability of the release in production.

While all the planning was going in full swing, we realised the team doing this should probably have a name, so that it would be easy for other GoTroops to reach out to us for anything related to migration.

So we went with X-Men 😋

Day-1: Update support library to 28.0.0

We updated the support library and built the app to tackle the compilation errors. We found major changes occurred by removing nullable parameters such as:

override fun onBindViewHolder(holder: ViewHolder?, position: Int)
override fun onBindViewHolder(holder: ViewHolder, position: Int)
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?) to override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?

…and so on.

Although it looks easy to remove nullable, it was quite a task due to the huge codebase. We approached this module-wise in pairs, and by the end of the day, the build was successfully running.

Day-2: Move to AndroidX

android.useAndroidX=true
android.enableJetifier=true

We added the above properties in the gradle.properties file, and ran the script which replaced all old support library package import and artifact names to new AndroidX names. (The script runs way faster on a clean build.) It took 20 minutes for the script to replace all the names. Yet, there were a few places the script could not replace, like the constraint layout, grid layout manager, and divider item decoration. So, we manually replaced those and added it to the script.

We realised that we had successfully finished the task, but due to the Jetifier tool, our build had increased in size. Eventually, the app size increased by 3MB. (This is something we’re now working to optimise further.)

Day-3: TargetSDK 28 update

We upgraded the target SDK to 28, which just required a change in the Gradle file. There were no compilation issues, but we did run into some runtime issues. The minute issues such as button margins, padding, loading images etc. were divided product-wise so that individual teams could pick up their team-specific issues for a faster fix. By the end of the day, we were successfully building the apk. ✌️

Day-4: Rebase changes

At last, we had to rebase our changes to our develop branch (who were ahead of us due to development from several teams on the same codebase). Our changes were majorly on the lines of removing nullables, imports, and codes affected due to this. Most of the development that happened on the develop branch was untouched, and finally, our build was ready to test.

Quality Analysis

And so it began — multiple sequential rounds of quality analysis as mentioned below:

  • Testing on develop branch for the initial phase
  • Testing after all the fixes for bugs reported in an early phase
  • Product-wise testing
  • Our regular full-cycle testing
  • Singapore app testing
  • Vietnam app testing
  • Thailand app testing

All right! After all the testing and bug fixing, we were ready to roll out the app to our users. 🎉

Rollout

We rolled it out successfully! But… now what? Monitoring live users and crashes had our heartbeats elevated, as the aim was to ensure our users did not get affected by us undertaking this task. Fortunately, we did not see any issues in production — everyone involved in this project did amazing work in finding and fixing detected bugs. Kudos to the team! 👏

At Gojek, we strongly believe in having pre-defied success metrics. In this case, we decided if the rollout gets to more than 40% of our consumer base and without bugs, then we are good. It’s not any magic number, but covers a large enough percentage of our users to mark this project as DONE ✅.

The Learnings

From the initial dry run phase to a huge team working on the migration process, this project was a tremendous learning experience. The experience one gains by being a part of a big project from scratch is unmatched. As multiple people from different teams came together to achieve the goal, we learned lots of things from each other. The whole process made me realise that effective communication with stakeholders is as important as any other part of the project, because that’s when we let them know why the migration process is critical to begin with!

Thanks again to the X-Men, for making this project a success. 🙌