Publishing libraries as a Java, Kotlin, or Android developer can be a complex and time-consuming process. Instead of focusing on development, we often find ourselves bogged down in configurations, setup, and version management. This was especially true for me when I first decided to publish a library. The challenge of even a one-time configuration was daunting, particularly because I didnβt want to rely on existing libraries. This made the task even more challenging.
Have you ever wondered why the publishing process has to be so complicated? Why canβt we just publish our libraries without all the usual headaches? These questions led me to a realization:Β Could there be a simpler and more efficient way to handle this process?
Thatβs when the idea of creating a custom Gradle plugin sparked in my mind β a solution that could eliminate all these complexities and make my work easier. Having experience working with convention plugins, I knew the power they could bring to managing dependencies and configurations. If youβre not familiar with these concepts, I recommend reading the articleΒ Mastering Android Dependency ManagementΒ to get a solid foundation.
In this article, Iβll take you on the journey I embarked on to create such a plugin. From the initial challenges to the final implementation, youβll learn how to do the same for your projects.
1. Setting the Stage: Creating the Core Class
To start, we need to create the following class. This class allows us to define the necessary information in the modules we want to publish, and weβll later read this information in the plugin:
open class ModuleInfoExtension {
var groupId: String = ""
var artifactId: String = ""
}
2. Simplifying Access: Adding an Extension Function
In the next step, to make accessing this information easier within the plugin, we create the following extension function:
val Project.moduleInfo
get() = extensions.getByType(ModuleInfoExtension::class)
3. Building the Foundation: Creating the Java Plugin
Next, weβll create the Java plugin for publishing our Java/Kotlin library, as shown below:
import your.package.name.configureJvmModulePublishing
import your.package.name.moduleInfo
import java.io.FileInputStream
import java.util.Properties
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.get
class JvmLibraryPublisherConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("maven-publish")
}
configureJvmModulePublishing()
configure<PublishingExtension> {
afterEvaluate {
publications {
create<MavenPublication>(
name = "androidFramework",
configuration = {
artifactId = moduleInfo.artifactId
groupId = moduleInfo.groupId
version = version
from(components["java"])
}
)
}
val nexusPropertiesFile = rootProject.file("nexus-config.properties")
val nexusProperties = Properties()
nexusProperties.load(FileInputStream(nexusPropertiesFile))
repositories {
maven {
name = nexusProperties["name"] as String
url = uri(nexusProperties["url"] as String)
credentials {
username = nexusProperties["username"] as String
password = nexusProperties["password"] as String
}
}
}
}
}
}
}
}
4. Configuring Module Publishing: JVM and Android
To make the process of publishing easier and more consistent across your project, Iβve included two key functions:Β configureJvmModulePublishing
Β andΒ configureAndroidModulePublishing
. These functions centralize the publishing configurations for both JVM and Android modules, ensuring that you donβt need to repeat the same settings in each module.
configureJvmModulePublishing
This function is designed to handle the publishing setup for JVM-based modules. It allows you to define and manage the necessary configurations in one place, simplifying the process of publishing your Java and Kotlin libraries.
internal fun Project.configureJvmModulePublishing() {
extensions.create("configurePublisher", ModuleInfoExtension::class)
}
configureAndroidModulePublishing
Similarly,Β configureAndroidModulePublishing
Β focuses on the Android modules in your project. It streamlines the setup by automatically configuring the necessary artifacts, such as source jars and Javadoc jars, making the publishing process more efficient.
Job Offers
internal fun Project.configureAndroidModulePublishing() {
extensions.configure<LibraryExtension> {
publishing {
singleVariant("release") {
withSourcesJar()
withJavadocJar()
}
}
}
extensions.create("configurePublisher", ModuleInfoExtension::class)
}
By using these functions, you can maintain a clean and consistent configuration across your entire project, reducing redundancy and potential errors in your publishing setup.
5. Streamlining Configurations: Centralizing Your Settings
Now, letβs break down the following configuration:
configure<PublishingExtension> {
afterEvaluate {
publications {
create<MavenPublication>(
name = "androidFramework",
configuration = {
artifactId = moduleInfo.artifactId
groupId = moduleInfo.groupId
version = version
from(components["java"])
}
)
}
val nexusPropertiesFile = rootProject.file("nexus-config.properties")
val nexusProperties = Properties()
nexusProperties.load(FileInputStream(nexusPropertiesFile))
repositories {
maven {
name = nexusProperties["name"] as String
url = uri(nexusProperties["url"] as String)
credentials {
username = nexusProperties["username"] as String
password = nexusProperties["password"] as String
}
}
}
}
}
Explanation:
The purpose of this configuration is to avoid duplicating the same settings across multiple modules. By centralizing these configurations within the plugin, we ensure that all modules within the project adhere to the same publishing setup without needing to manually replicate these settings in each module.
6. Making it Modular: Enabling Per-Module Configurations
The following line:
extensions.create("configurePublisher", ModuleInfoExtension::class)
enables you to open a lambda in each module and write the necessary configurations, as shown below:
plugins {
id("framework.android.publisher") // Don't forgot to add this
}
android {
namespace = "framework.network.ktor"
}
dependencies {
.....
}
configurePublisher {
groupId = "framework.network"
artifactId = "ktor"
version = "1.0.0"
}
Note: Iβll explain later how to create theΒ framework.android.publisher
Β plugin.
7. Registering Your Plugin: Integrating with Gradle
The following block of code is used to register your custom Gradle plugin:
gradlePlugin {
plugins {
register("frameworkJvmPublisher") {
id = "framework.jvm.publisher"
implementationClass = "JvmLibraryPublisherConventionPlugin"
}
}
}
Explanation:
gradlePlugin {}Β Block:
This block is used to define and configure custom plugins within your Gradle project. It specifies how the plugin should be registered and what class implements its functionality.plugins {}Β Block:
Inside this block, you can register multiple plugins by specifying an ID and an implementation class.register(“frameworkJvmPublisher”) {}:
This line registers a new plugin under the nameΒ"frameworkJvmPublisher"
. This is a unique name used within theΒgradlePlugin
Β block for reference purposes.id = “framework.jvm.publisher”:
TheΒid
Β is the unique identifier for the plugin. This ID is used in theΒplugins
Β block of a Gradle project to apply the plugin. For example, a project would useΒid("framework.jvm.publisher")
Β to apply this plugin.implementationClass = “JvmLibraryPublisherConventionPlugin”:
This specifies the fully qualified name of the class that implements the plugin. In this case, theΒJvmLibraryPublisherConventionPlugin
Β class contains the logic that will be executed when the plugin is applied to a project.
Before concluding, letβs also include a plugin specifically designed for Android libraries. This plugin helps streamline the process of publishing Android libraries by automating the necessary configurations.
class AndroidLibraryPublisherConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("maven-publish")
}
configureAndroidModulePublishing()
configure<PublishingExtension> {
afterEvaluate {
publications {
create<MavenPublication>(
name = "androidFramework",
configuration = {
artifactId = moduleInfo.artifactId
groupId = moduleInfo.groupId
version = version
from(components["release"])
}
)
}
val nexusPropertiesFile = rootProject.file("nexus-config.properties")
val nexusProperties = Properties()
nexusProperties.load(FileInputStream(nexusPropertiesFile))
repositories {
maven {
name = nexusProperties["name"] as String
url = uri(nexusProperties["url"] as String)
credentials {
username = nexusProperties["username"] as String
password = nexusProperties["password"] as String
}
}
}
}
}
}
}
}
Final Thoughts: The Power of Custom Gradle Plugins in Streamlining Your Workflow
Creating a custom Gradle plugin might seem daunting at first, but the benefits far outweigh the initial effort. By automating and centralizing your library publishing process, you can save time, reduce errors, and maintain consistency across all your projects. With the steps outlined in this guide, you are now equipped to build and customize your plugins, tailored specifically to your needs.
Remember, every challenge in development is an opportunity to create something better. If you encounter any issues along the way or need further assistance, feel free to reach out. Happy coding! π
Eager to Enhance Your Dependency Management?
Unlock the full potential of Gradle Version Catalog and Convention Plugins to optimize your Android projectβs dependency management. Transform your development workflow and put an end to version conflicts and compatibility headaches. If youβre looking to learn more about Version Catalog, Convention Plugins, and other advanced Gradle techniques, check out ourΒ ComposeNewsΒ project, where weβve implemented these concepts extensively. Dive into the code and see how these tools can revolutionize your Android development process!