Refactoring checkout flow with single activity architecture in android
Halodoc's humble start was a mission of simplifying healthcare across Indonesia. Starting with few people and an ambitious destination, executing quickly was the choice we took. This decision however, came with a price, technical debt.
In the above matrix, we fall into prudent quadrant. This is business as usual in fast paced environment and nothing unwise about it.
As time passed, our user base grew and with it our products & features. More and more changes started pouring in. For instance, we had to add features like various kinds of coupons, multiple entry points and maintaining different order states in our Pharmacy delivery flow. All such changes resulted in a lot of entropy.
Pharmacy delivery Checkout Flow
We will now look at the plethora of actions that a user can do in the checkout flow with respect to each screen in the checkout flow
- Cart screen user actions -
a. increase or decrease the quantity of the item
b. add or delete an item
c. completely clear the cart
d. change the delivery address
e. add or edit the notes of the delivery address
f. add or delete the prescriptions
- Payment screen user actions -
a. switch between dependent relative patients
b. apply or remove coupons
c. choose between multiple payment options
d. benefits of each payment option
- More payments screen user actions -
a. add multiple cards for payments
b. choose internet banking for payment
- Confirm screen -
a. once the order placed is successfully, user will be taken to the order details screen
Need for refactoring?
As stated above, the checkout flow is part of the Pharmacy Delivery(PD) module and was the high time for this flow to be refactored. Below were our observations and motivations to refactor
- Tweaking the existing features and adding new features in this flow to enhance the user experience was not scalable since this was written in MVC and our core business logic was muddled with view logic.
- Since there was no separation of concerns and core business logic was muddled with view logic, every new feature being requested in this flow was a nightmarish experience for the developers since this would result in breaking other existing features as most of the new features were dependent on the existing features.
- Having explained above factors, it was hard to maintain this flow and our business logic was solemnly untestable.
Essentially it was a big ball of mud!
Once we decided that this flow needs to be refactored as it was becoming highly unmaintainable, we went back to the drawing board. We started collating the data for refactoring and figured that checkout flow can be kicked-in from other different other flows in the app like the below scenarios
- User adds items from the categories/stores and gets into the cart.
- Doctors can refer a prescription in an online consultation and the user can enter into the cart with prescription details.
- User has added items in the cart and abandons the cart due to some reason, and after 'x' minutes of the abandoning if the user does not re-enter the cart then a local notification is triggered to remind them about the abandoned cart and they can enter the cart from that notification.
- User can re-order the items from the history.
With all the different entry points to the checkout flow and the data that needs to be injected, the flow itself started looking like a sub module in a module. Having collected above data points we came up with a reference architecture:
Responsibilities of each of the components in above reference architecture
a. PageController: Input controller for handling flow requests.
b. ApplicationController: Centralised point for navigation handling and providing dependencies
c. HumbleView: Untestable views with less/no business logic
d. PresentationModel: Platform independent representation of state and behaviour of the HumbleView. Helps in thorough testing.
e. Repository: For data fetching agnostic of the source. Maintaining data for the entire flow.
f. Gateway: For interaction with external entities like Payments, Doc viewer, etc
Adapting the Blueprint to Android
With the above blueprint of the architecture we decided, now it was time to adapt it to the android framework.
Below is the diagram which helps in understanding how each of the components of reference architecture are adapted with respect to android.
With the context of how to adapt reference architecture in Android, it was time to unlearn what we had learnt and re-learn to adapt to the new, and from the above diagram we can make out that this is the exact case of recently proposed and popularly accepted "Single Activity Architecture" pattern, with MVVM architecture and Repository Pattern.
Everyone proposes that "Single Activity Architecture" should be used for the entire app/module, but this was the right use case for us to evaluate how it fits it in with our existing other features set, and based on the results of this experimentation of how hard or easy it is for a developer to adapt, we would consider it for our future features implementation.
Components used in building
Following mentioned different Android Jetpack components are used in achieving the Single Activity Architecture with MVVM and Repository Pattern.
- Android KTX
- Navigation Component
Apart from the mentioned Android Jetpack components, Coroutines from the Kotlin framework and ArrowKt data types are used.
With the usage of Navigation Component, the user flow journey can be easily visualised using Navigation graph editor as seen below.
This was the phase of the converting all the architecture or diagrams into code and make everything work the way it did before. The user experience in the new flow with the completely changed system had to be the same as the older one. And to achieve this, maintain the sanctity of the architecture we had adapted and to ensure the scalability, maintainability, and testability of the code, we iterated multiple times on the discussions to follow the best practices to adapt. Below are the instances to name a few where we had to re-iterate to follow best practices
- As you saw in the background section of the blog, the plethora of features that needed to be handled especially on the cart screen, and the classic case of rendering the View State for each of those actions for both success and failure scenarios was getting messier with multiple Live Data emitting the different states of the view. Finally, we came up with the approach of maintaining the view state using a single live data and all the view states will be wrapped in
- Another instance of bummer was during the handling of the deeplink: where user selects the Go-pay payment option and they will be navigated out of the Halodoc app and once the payment is done, the user is brought back to the payment screen where the user had left and then the success or failure would be shown. The trick here was to handle the deeplink with the navigation component. Handling the scenario where the Halodoc app is killed by the Android OS due to a space crunch while the user had gone out of the app to make the payment in Go-pay and then bringing back the user to the same payment screen was tricky.
With all the things in place, the outcome was very successful, with following accomplishments
- As the complete flow now had the code in a well-written architecture, newer features were easily written with no hassle.
- The newly written code was fully testable and we did achieve a fair amount of coverage with the Unit Tests.
- No hotfixes were sent when the code was shipped to production. It was fully stable, as a rigorous testing activity was involved.
- Last but not the least, there was a tremendous amount of learning to completely switch a system without affecting a single user, and that is an extremely satisfying feeling for a developer!
We are always looking out to hire for all roles in our tech team. If challenging problems that drive big impact enthral you, do reach out to us at firstname.lastname@example.org
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke.
We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 1500+ pharmacies in 50 cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allows patients to book a doctor appointment inside our application.
We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates foundation, Singtel, UOB Ventures, Allianz, Gojek and many more. We recently closed our Series B round and In total have raised USD$100million for our mission.
Our team work tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.