Blog InfosAuthorErfan SnPublished21. January 2025Topicsandroid, Android App Development, AndroidDev, Gradle, KotlinAuthorErfan SnPublished21. January 2025Topicsandroid, Android App Development, AndroidDev, Gradle, KotlinFacebookTwitter

Identifying repetitive processes and striving to automate them with minimal friction is crucial for productivity.

For the Composable Screens project, I aimed to encourage more contributors to showcase their coding skills by automating some processes. This way, contributors could focus solely on converting designs to code, leaving fundamental tasks like module creation and integration to the Gradle build system.

To implement this idea, I had to complete three phases:

  1. Identify created modules and automatically introduce them as dependencies for the app module.
  2. Automatically invoke the defined entry point function within the created modules from the app module.
  3. Create a custom template to facilitate easier creation of new project modules.
Phase 1: Module Identification

Before anything else, I had to decide on the project structure and the modules’ hierarchy. After some thought and sketching, I created folders based on the design category and placed the modules within those folders.

qclay_trip module inside of travel category

 

Using the power of the Gradle build system, I would retrieve their paths and add them to the project.

To do this, I manually created a module in one of the categories as a test. Assuming several modules exist in a specific path and category, I wrote a function in the project’s settings file to traverse the folder, find all Gradle modules (ensuring they have a build script), and convert their paths to the Gradle module notation. Finally, I pass the result to the include function to register them as project modules.

// Project setting script file

private fun subprojects(path: String) =
file(path)
.listFiles()
?.filter {
it.isDirectory && it.listFiles()?.any { file -> file.name == "build.gradle.kts" } ?: false
}?.map {
"${path.replace('/', ':')}:${it.name}"
}.orEmpty()

include(subprojects("category/travel"))

After identifying them as project modules, to access their code in the app module, I needed to add them as dependencies.

Similar to the module introduction process, I sought a Gradle-based solution to avoid manually adding each one to the app module’s build script. Inspired by Gradle’s incubating Type Safe Project Accessors, I considered writing a custom iteration operator to gather the modules as project class instances and connect them. However, since the current APIs did not provide a list of modules, I used reflection to collect them as project class instances and looped through them to add them as dependencies to the app module. (Later, I realized I encountered XY Problem and unnecessarily complicated the solution)

// :app module's build script file

dependencies {
for (module in projects.category.travel) implementation(module)
}

private operator fun ProjectDependency.iterator() =
object : Iterator<ProjectDependency> {
var moduleCount = this@iterator::class.java.declaredMethods.size
override fun hasNext(): Boolean = moduleCount-- != 0
override fun next(): ProjectDependency =
this@iterator::class.java.declaredMethods[moduleCount].invoke(
this@iterator,
) as ProjectDependency
}

Phase 2: Automating Entry Point Invocation

Initially, my idea for this part was to write a Kotlin compiler plugin to modify the bytecode of the written code and use KSP to generate final consumable functions. This idea originated from a video on the “Code With The Italians” YouTube channel, which sparked the concept of annotating the module’s entry function and having the plugin alter bytecode based on metadata within the annotation. KSP would then collect these functions from various modules and add them to the app module’s nav host.

However, after deeper consideration, I realized that KSP alone, which offers a higher level of abstraction than Kotlin compiler plugins, would suffice. Like most developers, my experience with KSP was more as a consumer than a provider, so I created a playground project to familiarize myself with KSP by reading Kotlin’s quick start articles and building a simplified version of what I had envisioned.

During this process, I encountered a surprising limitation with KSP’s ability to gather annotated entities. In short, calling Resolver.getSymbolsWithAnnotation returns only symbols within the module being processed, even if entities in other project modules are annotated and connected as dependencies. The processor cannot handle them due to the lack of source code access. To confirm this limitation, I consulted Gemini AI in Android Studio, which explained that the KSP team designed it this way for simplicity and speed. As an alternative, Gemini suggested using Service Loader. Initially, this seemed appealing since I had never heard of it (though I later discovered KSP itself uses this method to register its processors). Upon researching its implementation, I found that Service Loader uses reflection, which prompted me to seek other solutions.

Next, I turned to ChatGPT, which recommended dependency injection frameworks as a solution. This seemed promising, although it required adding extra abstraction layers. Initially, I considered Dagger Hilt, but I opted for Koin for its Kotlin-friendly nature. While browsing Koin’s site, I discovered their new feature automating container creation with KSP. Curiosity about its workings led me to read its source code on GitHub, ultimately revealing a solution purely relying on KSP.

externalDefinitions = resolver
.getDeclarationsFromPackage("org.koin.ksp.generated")
.filter { a -> a.annotations.any { it.shortName.asString() == DEFINITION_ANNOTATION } }
.toList()

I discovered that KSP has a function, Resolver.getDeclarationsFromPackage, that retrieves all code in a specified package as KSP symbols, regardless of whether they reside directly in the module’s source set. This approach had some limitations but didn’t significantly impact the implementation.

At this point, I needed two processors: the first as a preparatory processor for created modules, generating code in a predefined package based on metadata, and the second specifically for the app module, collecting and preparing the generated code for consumption.

Job Offers

DiffUtils, Myers’ Algorithm and Jetpack Compose

APPLY NOW

Understanding Memory Leaks in Android & How LeakCanary Can Help

APPLY NOW

Automating Modularization And Wiring Them: A Gradle-based Approach

APPLY NOW

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Compose, droidcon San Francisco, Kotlin

Kobweb:Creating websites in Kotlin leveraging Compose HTML

droidconKobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David HermanEx-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David HermanEx-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David HermanEx-Googler, author of Kob ...

Jobs

No results found.

Phase 3: Custom Module Creation Template

First, I separated common build config and logic across project modules into precompiled script plugins in a separate Gradle build to facilitate reuse.

Building modules through Android Studio’s template section wasn’t customizable, and applying shared configs required repetition. To streamline this, I creatively defined a custom Gradle task for module creation.

Based on task arguments, the task determines the module category and name. Additionally, this method validates inputs, enforcing naming standards and restricting categories to existing project categories.

gradlew createCategoryTemplateModule -Pcategory={CategoryName} -Pmodule={module-name}

After creating a module this way, syncing the project allows Android Studio to recognize the generated files as project modules.

. . .

Conclusion

I automated module connection by first identifying modules in the settings file, then introducing them as dependencies in the app module’s build script, and finally completing the process with annotation processing using KSP. I further simplified module addition with a custom Gradle task. Overall, automation yielded the following benefits:

  1. Minimizing conflicts during project collaboration.
  2. Automatically placing created designs in their respective categories on the app’s main screen.
Join the Project

Au

Source: View source