Here’s is Part-2 of this series, where we talk about how we modularized our Gojek driver app for Android.
By Ashish Pathak
Before you delve into how we modularized our driver app for Android, here’s a refresher on why we decided to do so 👇
In a modularized application, the aim is to ensure proper segregation between the features with each feature fulfilling a specific purpose and is not bloated with unnecessary code. To achieve this, we also have a need for infrastructure code which is common across the features. This infrastructure code can be categorized into core and utilities groups. So, in general following 4 broad groups can be used as guiding principle:
Let us call these as groups. Only exception is the :app category which actually is a concrete module. It is an application gradle module. Any module that we create in the driver app should be part of one of these groups.
Group is a logical grouping of the modules. Think of a group as a namespace or the workspace or the logical grouping that contains other modules.
As a group is a high level logical construct that contains modules or other groups, we should define a small set of groups at the root level. Any additional group at the root level must be discussed and approved with the entire team.
Apart from the group at the root layer, we define a group for a feature. This group contains the modules like :api, :shared-ui, :data and :implementation.
If there is a need for a new group at the root level, we discuss that with the team before adding it.
We also added automated checks to see that no new group is created at the root level and inside :core and :utility groups.
Represents a library. It could be a java library or an android library or an application module. Modules are a part of any one of the groups we defined above. The :app module is the only exception to this rule.
In the feature group, there are the following modules.
:api module
A java library module that contains an interface for the exposed apis.
:shared-ui
It contains the shared UI elements. For e.g: components.
:cross-platform
A multiplatfom library module that contains non-android classes such as use-cases. These use-cases should be written in the platform independent way to support KMM.
:implementation
An android library module for the feature which contains the implementation details.
At the root there is the features group. This group has a set of features like say :featureA, the way it looks like in Android studio is as follows.
Fig: Shows feature structure with api, shared-ui and implementation modules
We also created following guidelines for the team to follow in regards to groups and modules.
Defining a module depends on which group we are creating a module in. Following approaches are recommend:
This android library module can only depend on other :api modules and not on any other android library implementation module except maybe :shared-ui module of that given feature.
For Horizontal Layering When we see the need to expose certain data or API from this feature to the outside world, we create a java library called :features::api for any requirement that feature has from its dependent module. The other API or android library modules can only depend on such java library modules which expose APIs and not on the implementation details.
Create :features::shared-ui module for sharing the Android specific UI logic from this feature to other features(eg: Component). Other features’ :implementation modules can depend on this to get that shared UI or functionality.
It is not recommended to create modules unnecessarily. This is to avoid bloating the source code with lots of unnecessary modules.
Here are some guidelines for when not to create a module:
We decided to layer our code horizontally with the common parts extracted in appropriate core and utility modules. Concept of horizontal layering is that each module as much as possible is self contained.
To add files, resources, or classes to the project, they should start in the implementation module of the feature. If the feature wants to expose them to the outside world, they should be moved to the api module. If they depend on the Android SDK, they should be moved to the shared-ui module. If they are data classes, they should be moved to the feature’s data module. If there are cyclic dependencies, they should be moved to the core or utility groups in the appropriate modules. To prevent poorly designed items from being added to core or utility, we wrote guardrails requiring review and whitelisting.
With all these guidelines and guardrails in place, we were all set to execute. We started extracting our first few modules. We quickly realized that there are certain tasks which are repetitive and boring. In Spite of that, those had to be executed with utmost accuracy. Also, when moving around some classes or packages, we saw a hell lot of errors being printed on the console which was overwhelming. But once we started addressing those one by one, we realized that some of those were really easy to solve.
After extracting out the first few modules, it was clear that for everyone on the team, this was boring to extract out modules. Here we came up with the idea of automating some of the repetitive steps. Automating some of those steps would mean more accuracy, reduced time for extraction and reduced cognitive load on the developer.
We started with writing simple grep and sed commands to automate some of the stuff like adjusting package names. Then finding out the names of resources being used in the given set of code. Finding out which classes are being used in the given set of code. While we were doing that, we realized that this is what we were doing most of the time when extracting feature:
So, using the command we were writing earlier, we wrote a very small set of scripts which automated these tasks for us. With these scripts in place, we saw that it reduced the time to perform these tasks for several hours to just under 5 minutes. So, we started using these scripts when extracting out the modules. We initially had planned to extract out only 2 modules in 2 sprints but we were able to extract 14 modules with these scripts in 2 sprints worth of time with just 2 developers working in parallel. Also, because these scripts were handling the boring and repetitive tasks, the developers were free to concentrate on the refactoring that they were interested in which allowed us to reduce the code entanglement problem to a great degree.
Now that we know how we approached modularizing the Driver app, in the next blog we will talk about the benefits we got from doing so.
Check out the final blog in this series, here:
Curious to know how we do what we do? Check out more blogs from our vault. 📄
Authors: Praveen Prashant, Kelvin Heng, and Deepesh Naini How we built a ML Driven voucher allocation engine to serve millions of customers across multiple geographies. The Idea 💡 How can we use different vouchers to get more business from our customer base while keeping our costs low? Achieving uplift in business
Gojek Jul 12, 2024 • 5 min readLet's delve into this conversation between two motion designers about finding a balance between ideation and execution.
Gojek Feb 2, 2024 • 3 min readPushing the limits with Courier and improving reliability with great numbers.