Vulcan is an Android Gradle plugin designed to simplify complex build configurations in multi-module projects. It empowers you to manage modular features and product flavors by defining swappable implementations for different functionalities and combining them into specific application variants.
In modern Android development using Clean Architecture, it's common for a single feature (represented by an interface) to require multiple implementations for different platforms or distribution channels. As a project scales, modularization becomes a necessity, but it introduces significant challenges:
- Complex Configuration: Manually managing and switching dependencies for implementation modules across different build variants is tedious and error-prone.
- Cluttered Build Scripts:
build.gradlefiles become bloated with conditional logic (if/elseblocks) to handle variant-specific dependencies, making them difficult to read and maintain. - Poor Scalability: Adding a new feature implementation or product channel often requires modifying build logic in multiple places, resulting in extensive rework.
Vulcan addresses these pain points head-on. It abstracts the interface-implementation relationship into the more intuitive concept of FlavorDimension-Flavor. Think of a Variant as the final dish you're serving, and Flavors as the customizable ingredients you choose for it (e.g., "spicy" or "mild").
By centralizing this logic in your root build.gradle.kts, Vulcan streamlines the setup of multi-module projects, making your build logic clearer, more robust, and easier to scale.
- Modular Feature Switching: Define features as "Flavor Dimensions" and provide multiple implementation modules for each.
- Variant Combination: Effortlessly combine implementations from different dimensions to create final application variants.
- Centralized Configuration: Manage variant-specific properties like
applicationIdandversionCodefrom a single location. - Concise Kotlin DSL: A clean, declarative DSL for configuring your build logic.
- Clean Dependency Management: Eliminate conditional dependency logic from your
build.gradlefiles.
Apply the plugin in your project's root build.gradle.kts file:
// build.gradle.kts
plugins {
id("io.github.valpha.vulcan") version "1.0"
}- Flavor Dimension: Represents a feature in your app that has multiple implementations (e.g., a
paymentdimension withgooglePayandwechatPayimplementations). - Flavor: A specific implementation within a dimension. Each flavor typically corresponds to a separate Gradle module (e.g.,
googlePay). - Variant: The final application build, created from a specific combination of flavors from different dimensions.
Let's break down how Vulcan is used in the testProject to see these concepts in action.
1. Module Structure
The project has two main features, func1 and func2, each with multiple implementation modules:
testProject/
├── app/ # Main application module
├── core/ # Core module for common logic or interfaces
├── func1-impl1/ # Implementation A for func1
├── func1-impl2/ # Implementation B for func1
├── func2-impl1/ # Implementation A for func2
├── func2-impl2/ # Implementation B for func2
└── func2-impl3/ # Implementation C for func2
2. Vulcan Configuration
In the root build.gradle.kts, we configure Vulcan to map features to their implementations and combine them into variants:
// build.gradle.kts
vulcan {
// 1. Define flavor dimensions and their available flavors (implementations)
flavorDimensions {
create("func1") {
flavors {
create("func1_1") { targetModule.set(project("func1-impl1")) }
create("func1_2") {
targetModule.set(project(":func1-impl2"))
flavorConfig { isDefault = true }
}
}
}
create("func2") {
flavors {
create("func2_1") { targetModule.set(project("func2-impl1")) }
create("func2_2") {
targetModule.set(project("func2-impl2"))
flavorConfig { isDefault = true }
}
create("func2_3") { targetModule.set(project("func2-impl3")) }
}
}
}
// 2. Define application variants by combining flavors
variants {
create("v12") {
targetModule.set(project("app"))
flavorMenu {
select("func1", "func1_1") // v12 uses implementation 1 of func1
select("func2", "func2_2") // v12 uses implementation 2 of func2
}
flavorConfig {
applicationId = "com.valpha.vulcan.v12"
versionCode = 40
}
}
create("v23") {
targetModule.set(project("app"))
flavorMenu {
select("func1", "func1_2") // v23 uses implementation 2 of func1
select("func2", "func2_3") // v23 uses implementation 3 of func2
}
flavorConfig {
applicationId = "com.valpha.vulcan.v23"
versionCode = 188
}
}
}
}3. How It Works
- Dimension-to-Implementation Mapping: The
flavorDimensionsblock maps an abstract feature (likefunc1) to its concrete implementation modules (like:func1-impl1). - Variant-to-Flavor Combination: The
variantsblock defines the final app builds. For instance, thev12variant is a combination of thefunc1_1andfunc2_2flavors.
When a developer selects the v12Debug build variant in Android Studio and syncs the project, Vulcan dynamically configures the app module's dependencies:
// Dependencies for the 'app' module under the v12Debug variant
implementation(project(":func1-impl1"))
implementation(project(":func2-impl2"))Switching to v23Debug automatically changes the dependencies:
// Dependencies for the 'app' module under the v23Debug variant
implementation(project(":func1-impl2"))
implementation(project(":func2-impl3"))This approach effectively decouples the main application from its feature implementations, centralizing the complex dependency-switching logic into a single, clean configuration block.
Contributions of all kinds are welcome! Please feel free to open a Pull Request with your ideas and suggestions.
This project is licensed under the MIT License. See the LICENSE file for details.