3 steps to reduce code redundancy in Volvo Cars App with custom Gradle plugins for Android Compose projects

Cansu Yeksan Aktaş
Volvo Cars Engineering
7 min readMay 24, 2023

I am an Android developer in a team dedicated to enhancing the driving journal feature in the Volvo Cars App. Our primary objective is to enhance the driving experience for users by enabling them to effortlessly record their trip data, while also providing the ability to edit, export, and filter this data as needed. Our work takes place in a dynamic multi-module environment, allowing us to access and modify various components and modules based on the project requirements.

As developers of the Volvo Cars App, we are responsible for managing over 200 modules. Recently, my team recognized an opportunity to optimize our build scripts by adopting Gradle’s recommended approach of utilizing custom convention plugins. By implementing this method, we were able to significantly reduce code redundancy within our build scripts. To put it into perspective, eliminating approximately 30 lines of code from each build script would result in a reduction of around 6000 lines overall. Through the utilization of custom build logic and plugins, we successfully minimized the occurrence of duplicated code within the Volvo Cars App’s build scripts.

Photo by codegrip

In this article, I will guide you through the process of introducing a standardized configuration using Gradle within the Volvo Cars App. Additionally, I will provide a demonstration of a simple project featuring multiple modules, including a Compose UI and a data layer. The demo project utilizes Kotlin’s domain-specific language (DSL), but rest assured that the same principles can be applied to your Groovy DSL build scripts effortlessly.

Initial problem

When creating a new Android project with Compose, the app-level build.gradle.kts file will generally resemble the structure shown below. Note that if you opt for Groovy as the build configuration language instead of Kotlin, the file may not have a .kts suffix.

As you can see, there are several repetitive units within the android{} block, such as defaultConfig, buildTypes, compileOptions, and so on. Additionally, there are compose-related units like buildFeatures and composeOptions, which are repeated in almost every UI module when using Compose. This can result in a lengthy build script.

Now, imagine working on a multi-module project with different features that include data, domain, and UI layers. In each feature module, you would end up with code repetition, using a similar script as shown above. Furthermore, having complete control over each build script can be challenging. For instance, let’s say it’s been decided to increase the minSDK level for every module in a multi-team project, but one team forgets to apply the change in their hardcoded build script. These types of issues can be overcome by creating a shared build logic through custom plugins and utilizing them across the project as needed. This approach eliminates the need for copying and pasting the same configurations repeatedly.

By implementing shared build logic through custom plugins, it becomes possible to remove everything inside the android{} block and eliminate the repetition of plugins and dependencies in each build script.

Solution

Let’s investigate the solution in a demo project with Compose. We are using Compose because we will also create a custom plugin for Android Compose. However, you can also apply the same logic for XML projects by ignoring the Android Compose plugin in the example project.

In this example, I will demonstrate a multi-module project with a module structure shown below:

Module structure of demo project

1. Setup

  1. Create an Android project with Compose.
  2. In the gradle folder of the root project, create a file named libs.versions.toml. Please note that versions might be different depending on when you are reading this article. By doing this, we are creating a version catalog:
Root project → gradle → libs.versions.toml

3. We can use the version catalog in the app-level build.gradle.kts:

App-level build.gradle.kts

4. Use it in the project-level build.gradle.kts too and sync the project:

Project-level build.gradle.kts

5. Let’s form the module structure of our project. First, create coredata directories under the root project. Then, create featurehome directories. We will add the build scripts later on. Currently, we are only adding directories.

6. Include modules in the project by adding them to settings.gradle.kts of the root project and sync the project:

Project-level settings.gradle.kts

The project setup is ready. We can now start creating shared build logic and custom plugins in it.

2. Create shared configurations and custom plugins

  1. Under the root project, create a directory named build-logic. Inside that directory, create a settings.gradle.kts file as shown below:
build-logic settings.gradle.kts

2. Inside build-logic, create a new directory: convention. This directory will include our plugin conventions. Create build.gradle.kts file under convention as shown below:

convention build.gradle.kts

3. In the convention directory, create srcmainkotlin directory. Under this, create a new directory named com.example.samples.apps.customplugingradle. You can change the name if you want. This folder will just hold our project extensions for creating plugins.

4. To apply the shared configuration, we will first need to add the includeBuild line to the project-level settings.gradle.kts file as shown below:

Project-level settings.gradle.kts

5. In the recently created directory, we will start adding the first project extension. In order to do it, create a Kotlin file KotlinAndroid.kt and paste the code below in the Kotlin file. As you see, it holds a few Android tag properties in our build scripts, such as compileSDK, minSDK, compileOptions, and kotlinOptions.

Project extension for configuring Kotlin Android

6. We will create a similar file for Compose configurations as well. You can skip this step if your project is not in Compose. Similarly, the file name will be AndroidCompose.kt, and the script of the file is as shown below. As you can see, it holds a few Compose-related build properties, as well as Compose-specific dependencies often used across Compose modules.

Project extension for configuring Android Compose

7. Under srcmainkotlin directory in convention, let’s create our first convention plugin. In order to do that, create a Kotlin class named AndroidLibraryConventionPlugin as shown below. As the name implies, this will be our library convention plugin.

Note that instead of using the version catalog in the plugins section, we are directly hardcoding the library names (reference).

Android Library Convention Plugin

8. Similarly, in the same directory, create a new Kotlin class for Compose convention plugin: AndroidLibraryComposeConventionPlugin. Paste the following code in:

Android Library Compose Convention Plugin

9. As the project extensions and plugins are complete, we can now configure the build.gradle.kts of the convention module. We will add the code snippet shown below to the existing build.gradle.kts. If you want, you can change the plugin ID. In the example below, com.example.customplugingradle.android.library was used. You can follow the rules in the reference for the naming convention of plugin IDs.

convention build.gradle.kts

10. Sync the project.

3. Apply the shared configurations across the project

  1. Let’s configure the build script for the coredata directory. Create a build.gradle.kts file under the data directory, and paste the code below (you can change the namespace according to your project):
data build.gradle.kts

In this build script, we are adding retrofit, kotlinx-serialization, and okhttp dependencies, as well as junit4 test dependency, in addition to our custom plugin for Android Library com.example.customplugingradle.android.library. You can see how short it gets compared to a build script created by default. The reason we added those dependencies is that the data module generally consists of API-request dependencies.

2. Configure the featurehome directory too. To do this, add build.gradle.kts, including the code snippet below, into the home directory:

home build.gradle.kts

Note that this directory will be a Compose module including home screen, and this is why we have also included the compose plugin this time, namely com.example.customplugingradle.android.library.compose.

When you sync the project, the build should be successful. That means that applying shared configurations is complete. If you get any kind of build error, read the error message carefully and modify target dependencies/versions accordingly, if needed.

Now, all the dependencies are ready to be used across the different modules of the project as needed 🎉

Conclusion

In addition to the benefits described previously, we have successfully implemented custom plugins in the Volvo Cars App. This has significantly improved our Android development process.

By adopting custom plugins, we were able to effectively manage the complexity of our multi-module project, which includes various features, data layers, domain layers, and UI components. The centralization of common build configurations and dependencies within the custom plugins reduced code repetition across modules. This resulted in cleaner and more maintainable codebases.

Furthermore, the custom plugins ensured consistency in build scripts throughout our project, facilitating smoother collaboration among our multiple teams that are involved in the development process. We could easily update and apply changes to the shared plugins, avoiding discrepancies or oversights in build configurations. This was particularly valuable in ensuring that all modules remained up-to-date with essential updates, even when different teams were working on them.

Overall, the adoption of custom plugins in the Volvo Cars App showcases the significant benefits that they offer in addressing common challenges in Android development. We have experienced improvements in code quality, maintainability, and collaboration, through the use of custom plugins. Our positive experience highlights the potential of this approach as a powerful tool for enhancing the development process in Android applications.

For more information, please check the references below, as well as the reference project Now in Android from Google:

Finally, please find the source code of the demo project in the article here.

Special thanks to Neha Garg for her help in this article 🙏🏻

--

--