Last year we were all stuck at home, our lives had changed drastically - from enjoying the social interactions to being socially distanced. Moving from an office culture to a work from home one amidst a global pandemic had it pros and cons.
It was around this time - when the world had come to a stand-still, businesses paused, and plans to kick-start the affected economies were researched upon.
We @ BookMyShow have always been conscious of Tech-Debt, and this seemed like a perfect opportunity to reflect on it and make considerable inroads to speed up the way it was handled. One of the areas that we as a team had been talking about was React Native (RN) removal - but there were always new deliveries to undertake and new features to ship out - which didn't allow the team to fully focus on it.
During the late 3rd quarter of 2020, I decided to take up the initiative to migrate the pending RN screens to Native (Kotlin) and completely remove React Native from the app.
After pitching and discussing it with the team, RN removal as a primary task was pitched to the then Engineering Lead - and we got a go-ahead. This also meant, convincing the iOS counterparts, and the team lead given the focus would be on core engineering tasks. But why did we decide to move away from RN?
Why no React Native?
- It's not always in sync with latest Android tech - takes time for the RN team to implement.
- The primary objective of having to write it once was defeated with the hybrid model (native + react native) because for every screen we migrated, there was a need to write a lot of logic around passing screen related data and views (if required). This boilerplate was also a reason for quite a few crashes.
- When we integrated React Native, there was no multi-lingual support, hence we were maintaining 2 different code-bases, one for English and another for different languages - which demanded that we make changes twice.
- We found out ways to bring React Native advantages to native, hence maintaining another skillset wasn't really required.
Here's how it went -
Initially, we started slow - worked on time and effort estimations, decided on a timeline, tried to align some product and design folks, and started learning React Native (we had to understand the code enough and the use cases around it to rewrite it back into native).
Movie ShowTimes -
The first screen in my migration effort - one of the most visited screens in the app. I started by looking through the existing UI and created a mental architecture of how it would be built.
Then I took this opportunity to pitch my architecture to a few senior Android devs within the team, discussed implementations, and sketched out a plan to create modular UI components and try out new approaches such as Coroutines.
Learning - Make sure to discuss your implementation strategy with your peers before you sit down to write your code. Discussions give us multiple insights before we actually implement, which is very powerful. This will give you feedback and save you from refactoring (a lot) when your PR (pull request) is being reviewed. Incredible!
Two heads are better than one. And not just twice better, many many times better — Dan Brown
How did we go about the implementation?
After scoping out the view hierarchy, I figured that there are a couple of custom components that were needed to be built, and hence got started with those. I made sure that these UI widgets were extensible - exposed correct data and callbacks - and if required, easy to modify their logic and design based on future use-cases. This made sure that the UI behavior would not need to be replicated in multiple screens.
Post creating the standalone components, the next step was to create the container screen and start working on the API layer. I wanted to use coroutines, and along with that also took this opportunity to try out the clean architecture pattern -
This helped me avoid the "God ViewModels" and separate out the business logic. Hence, ViewModel then was only responsible for converting the processed data that it got from the use-case into models that view could understand. Furthermore, there was no Android-related code in the Use Case hence the business logic was perfectly setup to write unit tests.
Learning - Be open to change; learning the new stuff and unlearning the old.
While working with huge lists in the Movie ShowTimes screen, I also learned a lot about using Kotlin Collections optimally. It was exciting how we can completely get rid of the iterator logic for processing large lists and use chained functions to compress the code into just a few lines. Beautiful!
Movie Format Selection
Before we could finish working on Movie ShowTimes - another important task came up! The team was revamping BookMyShow's Design System and since we had to work on Bottom Sheets as part of the migration - I took it upon myself to deliver this. This included a lot of conversations with the product designers to define the look and feel, and the bottom sheet behaviors in various cases.
This gave me the opportunity to build the base for the bottom sheets - which included exposing interaction and other callbacks and writing required extensions - so that new bottom sheets could be set up very quickly. I then moved back to complete the Movie ShowTimes screen.
Post completion of the Movie ShowTimes, we finally started working on one of the complicated screens in the app - Seat Layout.
Deciding to follow the similar clean architecture approach - we started understanding the UI components required. I went ahead and built the Quantity Selector native component as a custom view.
The base is a SeekBar on which the thumb moves. The track is yet another custom component, which gets the list of numbers. We initially, calculate the total width, the padding, the width of each text in the list, and the individual spacing required, and then the texts are dynamically drawn on a canvas bed. The SeekBar is responsible to control the UI and only exposes the callbacks back to the parent custom view which then handles other UI related stuff.
Data Managers Deprecation and Way Forward
Moving on, I dove into the actual Seat Layout screen and structured it according to the architecture. The code then revealed that there was also a lot of data being passed from screen-to-screen starting from Movie ShowTimes up until the end of the booking flow.
This was so far maintained using Singleton Model classes which stored the data. These were a big pain point because -
- The data was saved as models - which were tightly coupled with API response models. If in the future the response changes, we will have to change the reading and writing of data - not scalable.
- There was a parent classes which contained multiple child models (to hold different sets of data). Each of these classes could either be self-created or creating using the parent. Since there was not a single approach to initialize them - this was a huge problem - because different methods would generate completely different objects. You might be setting the data to one object and trying to read from another which would understandably return a null value.
Noticing these problems, I sketched out an approach and again pitched it to a couple of team members. The approach included using abstracting the singleton models by using providers to set data going forward (into an inner class which can never be initialized from outside the provider) The provider implementation then was responsible to internally create the singleton classes in a correct manner and then retain the data across multiple screens.
This seemed to be a big win for the team!
The stitching up of the screens with the screens on either side of the flow bought an end to the migration effort. There were a couple of hot fixes required - we faced some crashes and bugs - but the staged rollout (and the pandemic 😅) limited the impact and we as a team learned a few more lessons!
It is so easy to overestimate the importance of one defining moment and underestimate the value of making small improvements on a daily basis. Too often, we convince ourselves that massive success requires massive action.
The migration wasn't limited to just the screens mentioned. This was a continuous effort by a couple more peers, who migrated other screens away from React Native and into Native. We also had 2 different booking flows and we managed to combine the different seat layouts into one.
Finally, post the migration we decided to completely remove the React Native library from the app. During this process, I discovered a lot of dead code lying around - which was cleaned up too.
This gave us a lot of benefits -
- We removed a complete tech stack, and saved more than 35% on the app size. Previously the average app size was around 30.6MB!
- Kotlin in our code base went up from just 4% in April 2020 to more than 44% in April 2021.
- Our build times went down by multiple minutes which resulted in increased developer productivity.
Post the release we also extracted some numbers from Firebase. Here's a look -
This was a huge focused effort, around a quarter to migrate the screens and another to remove the library, but we kept pushing the code out iteratively and achieved brilliant results in the process.