Background Image

2024-08-04

Preparing For Release

Ryan Scott image

Introduction

Now that we have a useful bit of multiplatform UI, it's time to share that code with the world. This is article 5 in the series about creating a Compose Multiplatform UI library. The previous article was the final article in the series that focused on improving the look of the segmented display. In this article, we'll perform the less-glamorous task of preparing our library for release in a way that can be easily consumed. In the next article, we will actually release our library to Maven Central.

At the end of this article, you will have:

  1. A repository set up for communicating about your library
  2. The ability to generate reference documentation with some custom elements in a way that will be picked up after publishing to Maven Central
  3. Publishing configuration such that your library sources, the documentation, and the assembled binary are all published to the publishing destination of your choice with a pom xml file that describes your library.

TLDR

  1. Add a license file
  2. Add the README.md files
  3. Add the dokka gradle plugin to your library project
  4. Add kdoc comments on all public references
  5. Generate your reference documentation site locally
  6. Configure gradle to publish our library locally
  7. Check what will be published by inspecting your local maven repostory

Prepping your project for release

Even though you may feel that your code is totally self-explanatory and that it should be obvious to the rest of the world how to get the code and use it and what the implications are if they were to use your software.

Adding an open-source License

The world of open-source licenses is quite complex. I'm not a lawyer, and I cannot provide legal advice. In this article, we'll use Apache-2.0, but that is not meant to influence your decision on what license to use. If you're looking for a starting place to research licenses, Wikipedia has a comparison of software licenses. The simplest way to download this license is to curl it. In a command prompt, navigate to the root directory of the project and run the following:

$ curl -o LICENSE https://www.apache.org/licenses/LICENSE-2.0.txt

Adding the README.md files

Now that we have the license file, let's take care of helping visitors to our repository successfully navigate our project. The README.md file is the first thing that visitors to your repository will see. It should contain a brief description of the project, how to get the code, how to use the code, and how to contribute to the project.

The root README.md

This file should be seen as the front page of your project, and often, many people will not look further at your project. Let's get started by creating a README.md that just has a screenshot added to the top.

First, run your project on one of the platforms (I chose web):

./gradlew :composeApp:wasmJsBrowserRun

Then take a screenshot of the output. I called the screenshot readme_headline.png. Now put the file in the docs/images directory of your project (if the directory doesn't exist already, create it).

Now create the README.md file in the root of your project (if it doesn't exist). and add the following:

# segmented-display

![Segmented Display](docs/images/readme_headline.png)
Note

The line # segmented-display above should actually be the name of your project.

If the README.md file already exists, just delete the contents and add the above.

Now you'll want to describe your repository and it's main offering.

Segmented displays are ubiquitous in the world of electronics. They are used in everything from digital clocks to thermostats to calculators to microwave ovens. This library provides a Composable function that can be used to create and style a segmented display in your Compose Multiplatform application.

Next, we'll want to describe the supported platforms:

## Supported Platforms

Because this library is a [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) library, the following platforms are supported:
* Android
* iOS
* Desktop (via JVM)
* Web (via WASM)

Finally, we'll explain the project structure:

## Project Structure

There are two modules in this project:
* [segmented-display](segmented-display/README.md), the library module that you can depend upon in your Compose Multiplatform project
* [composeApp](composeApp/README.md), a demo application that shows the main library features
Note

It may be wise to add a gif that will help you demonstrate the library in action somewhere on the root README.md file. You can see this article I previously wrote regarding how you can make such a gif.

After we've actually published our library, we'll add a section on how to get the library from Maven Central, add version badges, etc. Next, we'll add the README.md files for the segmented-display module and the composeApp module.

The segmented-display README.md

This README.md should be the landing spot for people who are interested in using, extending or reading your library code. While this should not be the reference documentation, it should point to some of the important parts of the code, such as the main composable functions. It should also give some advice regarding usage and extension.

Create the README.md file in the segmented-display directory of your project (if it doesn't exist) and add the following:

# segmented-display

This library provides the following Composable functions that can be used to create a segmented
display:
* [Rect7SegmentDisplay](src/commonMain/kotlin/com/fsryan/ui/segments/Rect7Segment.kt)
* [Hexagonal7SegmentDisplay](src/commonMain/kotlin/com/fsryan/ui/segments/Hexagonal7Segment.kt)

Of these, the one you'll most likely want to use is [Classic7SegmentDisplay](src/commonMain/kotlin/com/fsryan/ui/segments/Classic7SegmentDisplay.kt) because the rectangular version is visually unappealing.

## Usage

The most basic usage of the `Classic7SegmentDisplay` composable function is as follows:

```kotlin
Classic7SegmentDisplay(
    modifier = Modifier.fillMaxWidth().aspectRatio(0.5F),
    text = "01234567"
)
```

However, you can also style the display in a number of ways:

```kotlin
Classic7SegmentDisplay(
    modifier = Modifier.fillMaxWidth().aspectRatio(0.5F),
    text = "01234567",
    shearPct = 0.18F,
    topAreaPercentage = 0.45F,
    thicknessMultiplier = 0.75F,
    gapSizeMultiplier = 1.25F,
    activatedColor = Color.Red,
    debuggingEnabled = true,
    angledSegmentEndsOf = ::createAsymmetricAngled7SegmentEndsFun
)
```

## Developing your own segmented display

If you want to develop your own segmented display, you can use the more-generic [SingleLineSegmentedDisplay](src/commonMain/kotlin/com/fsryan/ui/segments/SingleLineSegmentedDisplay.kt) composable function as a starting point and then implement your own `renderCharOnCanvas` function. As for how to implement your `renderCharOnCanvas` function, you could use the [drawRect7SegmentChar](src/commonMain/kotlin/com/fsryan/ui/segments/Rect7SegmentDisplay.kt#L57) and [drawClassic7SegmentChar](src/commonMain/kotlin/com/fsryan/ui/segments/Classic7SegmentDisplay.kt#L61) functions as examples.

Then you can pass your new function to the `SingleLineSegmentedDisplay` composable function as the `renderCharOnCanvas` parameter as below:

```kotlin
SingleLineSegmentedDisplay(
    modifier = Modifier.fillMaxWidth().aspectRatio(0.5F),
    text = "01234567",
    shearPct = 0.15F,
    renderCharOnCanvas = { idx: Int, char: Char, offset: Offset, charWidth: Float, charHeight: Float ->
        // your implementation that draws a vertical segmented display character here
        
    }
)
```

As you can see above, some basic hints about usage, extension and some links to relevant code are provided.

The composeApp README.md

For the sample app, the most importatnt point is to show how the app can be run on the various platforms.

# FS Segmented Display Sample App

This app is intended to enable basic manipulation of some of the properties you can style.

## Running
* in a browser: `./gradlew :app:wasmJsBrowserRun`
* In a desktop window: `./gradlew :app:desktopRun -DmainClass=MainKt`
* On an Android device: `./gradlew :app:installDebug && adb shell am start -n com.fsryan.ui/.MainActivity`
* On an iOS device: it's just best to use Android Studio plus the [Kotlin Multiplatform plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform)

Automated reference documentation

Now that we've created a very basic tour of our library for visitors of our repository, we should provide more comprehensive documentation for developers who want to use our library. It's important to not skip this, as, if we publish it correctly, we'll have our versioned reference documentation available for us via javadoc.io. Moreover, Maven Central forces you to provide a javadoc.jar file. In the next article, we'll package up our reference documentation in a javadoc.jar and publish it with our library.

Adding the Dokka Gradle Plugin

Dokka is the tool we'll use to generate our reference documentation website. To add Dokka to your project, first declare the plugin dependency in your gradle/libs.versions.toml file:

[versions]
# other versions
dokka = "1.9.20"

[libraries]
# libraries

[plugins]
# other plugins
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }

Then we must apply the dokka plugin to our segmented-display module. Add the following to the plugins block of the segmented-display/build.gradle.kts file:

plugins {
    // other plugins
    id("org.jetbrains.dokka") version versions["dokka"]
}

Finally, sync the project, and you now should be able to generate your reference documentation website.

Adding KDoc comments

In contrast to typical javadocs, Kotlin uses KDoc comments. These comments are similar to javadocs, but they are written in markdown. You should add KDoc comments to all public references in your library. For example, on our transformCharToActiveSegments function:

/**
 * A basic transformation function that translates a character to the active
 * segments that can be used for determining the segments of a 7-segment
 * display that are active. The active segments are described withing the 7
 * least significant bits of the returned integer and are mapped as follows
 * (where the number shown be low is the index of the bit from least to most
 * significant):
 * 
 * ```
 *  1111
 * 2    3
 * 2    3
 * 2    3
 *  4444
 * 5    6
 * 5    6
 * 5    6
 *  7777
 * ```
 * 
 * > [!NOTE]
 * > If the character is not in 0-9a-zA-Z\-, then 0 will be returned
 * 
 * > [!NOTE]
 * > Capitalization does nto make a difference
 * 
 * @param char the character to translate
 * @return the active segments for the character
 * @author fsryan
 */
fun transformCharToActiveSegments(char: Char): Int = when (char) {
    /* ... */
}

Here we've added a KDoc comment that explains what the function does, what the function returns, and what the function does in the case of an invalid character. Additionally, using @param and @return, we've described the inputs and outputs. We've also documented the author of the function using the @author marker. Writing good KDoc comments is often difficult. Here's a list of things to avoid:

  • Repeating the function signature in the KDoc comment
  • Documenting your identifier immediately as you're writing it. Because things tend to change, this can lead to confusing and wrong documentation. The only thing worse than undocumented code is wrong documentation. In general, it's better to wait until you are done with the implementation before writing the KDoc comment.
  • Excessive verbosity in the KDoc comment. If you need to explain a lot, this is a sign you may need to refactor

Here are some things to consider as you write your KDocs:

  • Keep your KDoc comments brief and descriptive.
  • Inputs, outputs, and edge cases (Inputs use the @param marker and outputs use the @return marker; there is no specific marker for edge cases)
  • How the documented identifier is intended to be used
  • Exceptions that could be thrown and thus, must be handled by the caller (using @throws)
  • Navigability between kdocs on related code. When there is strongly related code, you can use @see to link to another identifier's KDoc or you can add markdown links.
  • If your library has library-specific nomenclature, it's a good idea to not assume that the user knows what you're talking about. It's helpful to link to the explanation of that nomenclature where it is used.

I'm not going to go through every identifier here, but you should add KDoc comments to all public identifiers in your library.

Generating the reference documentation site locally

Let's take a look at our documentation site. To generate the reference documentation page with gradle:

./gradlew :segmented-display:dokkaHtml

This will generate the documentation in the segmented-display/build/dokka directory. Open the index.html file in your browser to see the documentation. On OSX, you can open the reference documentation in your browser by:

open segmented-display/build/dokka/index.html

As you can see, however, our documentation page looks a little sparse:

Initial Documentation Landing Page

But if we click on the one package name and then on the "Functions" tab, we can see the documentation we've added.

Documentation Text Sample

Another thing you'll notice is that our documentation page is cluttered with the Compose preview functions and helper functions we added for the previews. These functions obscure the public identifiers we want our consumers to use and understand. Because Dokka, by default, only generates documentation for public identifiers, we can fix that by changing the visibility modifier of those preview functions to internal. So for all the compose preview functions we've written, just change fun to internal fun.

Now, regenerating the documentation, we have only the identifiers we want our consumers to know about.

Improving the look of your reference documentation

For a tool that helps create documentation, it's ironic, but Dokka does not document its features well. However, you can style your documentation site and add to it. Let's start by adding some content for the segmented-display module. To see how it works, create a file called segmented-display/MODULE.md, and add the following:

#Module segmented-display

This is a Compose Multiplatform library that targets Android, desktop, iOS, and WASM JS.

Then include this markdown file in the dokka configuration of your segmented-display/build.gradle.kts file:


tasks.withType<DokkaTask> {
    dokkaSourceSets.configureEach {
        includes.from("MODULE.md")
    }
}

Now, if you regenerate your documentation site, you should have the following:

Modified Documentation Landing Page

You can add anything you want to this markdown file, as long as the content is markdown. You can link to external or bundled images, link to your company's website, etc. Below, we'll do the following:

  1. Bundle our docs/images/readme_headline.png image with the documentation site
  2. Reference the image in the MODULE.md file
  3. Add a custom footer message
  4. Add a link to the FS Ryan Software website
#Module segmented-display

This is a Compose Multiplatform library that targets Android, desktop, iOS, and WASM JS.

![Sample Usage Image](./images/readme_headline.png)

Notice above that we changed the module name and that we referenced the image with a relative path. Now we need to tell dokka both to bundle the image, to change the module name, and to add a custom footer message:

In the segmented-display/build.gradle.kts file, add the following:

tasks.withType<DokkaTask> {
    val dokkaBaseConfiguration = buildString {
        append("{\"customAssets\": [\"")
        append(rootProject.file("docs/images/readme_headline.png"))
        append("\"],")
        append("\"customStyleSheets\": [],")
        append("\"footerMessage\": \"(c) 2024 FS Ryan Software\"")
        append("}")
    }
    pluginsMapConfiguration.set(
        mapOf("org.jetbrains.dokka.base.DokkaBase" to dokkaBaseConfiguration)
    )
    dokkaSourceSets.configureEach {
        reportUndocumented.set(true)
        includes.from("MODULE.md")
    }
}

That will result in something like the following after regenerating the documentation page:

Customized Documentation Landing Page

You can customize the look of your documentation site in many more ways, however, We'll stop here for now. See the Dokka HTML documentation for more information.

Configuring Gradle to publish our library

"POM" stands for "Project Object Model." In other words, a pom file is a document that describes your project. You can learn more about it from the apache maven website. It's a very mature description of a project that is ubiquitous throughout the Java/Kotlin ecosystem. After we include the maven-publish plugin in our project, we'll need to configure the pom file to provide information about our project.

Including the maven-publish plugin

To include the maven-publish plugin, add the following to the plugins block of the segmented-display/build.gradle.kts file:

plugins {
    // other plugins
    `maven-publish`
}

Initial configuration of the maven-publish plugin

This will give you access to the MavenPublication class in your segmented-display/build.gradle.kts file. Sync the gradle project to enable code completion.

Multiplatform publishing is only slightly more complicated than single-platform publishing. The biggest difference is that we need to configure many different publications--not just one. Therefore, adding an extension on top of MavenPublication that configures each publication will help us maintain uniformity among each publication. Add the following at the top level of the segmented-display/build.gradle.kts file:

// A function that will configure the maven publishing for a publication
fun MavenPublication.configureMultiplatformPublishing(project: Project) {
    val publicationName = name
    with(pom) {
        description.set("$publicationName target of the Compose Multiplatform FS Ryan library for rendering segmented displays")
        inceptionYear.set("2024")
        url.set("https://github.com/fsryan-org/segmented-display")

        issueManagement {
            url.set("https://github.com/fsryan-org/segmented-display/issues")
            system.set("GitHub Issues")
        }

        licenses {
            license {
                name.set("The Apache Software License, Version 2.0")
                url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
                distribution.set("repo")
            }
        }

        developers {
            developer {
                id.set("ryan")
                name.set("Ryan Scott")
                email.set("ryan@fsryan.com")
                organization.set("FS Ryan")
                organizationUrl.set("https://www.fsryan.com")
            }
        }

        scm {
            url.set("https://github.com/fsryan-org/segmented-display.git")
            developerConnection.set("scm:git:git@github.com:fsryan-org/segmented-display.git")
        }
    }
}

There's a lot here, so lets break it down:

The val publicationName = name line is there because both the MavenPom class (which is the default receiver of all calls inside the with block) has a Property<String> getter function called getName, which clashes with MavenPublication.name, which is a String. Inside of the with block, we're configuring the various parts of the pom file. The issueManagement, licenses, developers, and scm blocks will be used by Maven Central to direct users who are looking at our library to our repository page on GitHub as well as provide a point of contact for the developer who produced the library. The licenses block is also important in that it communicates the open source license we used to license our library.

Then, also in the segmented-display/build.gradle.kts file, add the following to the publishing block:

publishing {
    publications.withType(MavenPublication::class.java) {
        configureMultiplatformPublishing(project)
    }
    publications.whenObjectAdded {
        (this as? MavenPublication)?.configureMultiplatformPublishing(project)
    }
}
Note

We're actually publishing seven publications: common (which is unmarked), android, iosArm64, iosSimulatorArm64, iosX64, jvm, and wasm-js.

Note

Each publication has its target marked with an indicator at the end of the name. For example, the wasm-js publication is called segmented-display-wasm-js.

Why do we need to add the publications.whenObjectAdded call? The Android Gradle plugin defers creation of the android release publication. This block allows us to catch these deferred publications and configure them in the same way we configured the other publications.

Here, you can stop and check that you have all the correct publishing tasks:

./gradlew :segmented-display:tasks --group=publishing

The output should look like this:

------------------------------------------------------------
Tasks runnable from project ':segmented-display'
------------------------------------------------------------

Publishing tasks
----------------
generateMetadataFileForIosArm64Publication - Generates the Gradle metadata file for publication 'iosArm64'.
generateMetadataFileForIosSimulatorArm64Publication - Generates the Gradle metadata file for publication 'iosSimulatorArm64'.
generateMetadataFileForIosX64Publication - Generates the Gradle metadata file for publication 'iosX64'.
generateMetadataFileForJvmPublication - Generates the Gradle metadata file for publication 'jvm'.
generateMetadataFileForKotlinMultiplatformPublication - Generates the Gradle metadata file for publication 'kotlinMultiplatform'.
generateMetadataFileForWasmJsPublication - Generates the Gradle metadata file for publication 'wasmJs'.
generatePomFileForIosArm64Publication - Generates the Maven POM file for publication 'iosArm64'.
generatePomFileForIosSimulatorArm64Publication - Generates the Maven POM file for publication 'iosSimulatorArm64'.
generatePomFileForIosX64Publication - Generates the Maven POM file for publication 'iosX64'.
generatePomFileForJvmPublication - Generates the Maven POM file for publication 'jvm'.
generatePomFileForKotlinMultiplatformPublication - Generates the Maven POM file for publication 'kotlinMultiplatform'.
generatePomFileForWasmJsPublication - Generates the Maven POM file for publication 'wasmJs'.
publish - Publishes all publications produced by this project.
publishIosArm64PublicationToMavenLocal - Publishes Maven publication 'iosArm64' to the local Maven repository.
publishIosSimulatorArm64PublicationToMavenLocal - Publishes Maven publication 'iosSimulatorArm64' to the local Maven repository.
publishIosX64PublicationToMavenLocal - Publishes Maven publication 'iosX64' to the local Maven repository.
publishJvmPublicationToMavenLocal - Publishes Maven publication 'jvm' to the local Maven repository.
publishKotlinMultiplatformPublicationToMavenLocal - Publishes Maven publication 'kotlinMultiplatform' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
publishWasmJsPublicationToMavenLocal - Publishes Maven publication 'wasmJs' to the local Maven repository.

Notably, we don't see any publishing tasks for our Android release publication. Let's fix that. In the segmented-display/build.gradle.kts file's kotlin block, we configure the androidTarget target. Add the following line to that block:

androidTarget {
    publishLibraryVariants("release")
    /* ... */
}

Now when we run:

./gradlew :segmented-display:tasks --group=publishing

We should see:

------------------------------------------------------------
Tasks runnable from project ':segmented-display'
------------------------------------------------------------

Publishing tasks
----------------
generateMetadataFileForAndroidReleasePublication - Generates the Gradle metadata file for publication 'androidRelease'.
generateMetadataFileForIosArm64Publication - Generates the Gradle metadata file for publication 'iosArm64'.
generateMetadataFileForIosSimulatorArm64Publication - Generates the Gradle metadata file for publication 'iosSimulatorArm64'.
generateMetadataFileForIosX64Publication - Generates the Gradle metadata file for publication 'iosX64'.
generateMetadataFileForJvmPublication - Generates the Gradle metadata file for publication 'jvm'.
generateMetadataFileForKotlinMultiplatformPublication - Generates the Gradle metadata file for publication 'kotlinMultiplatform'.
generateMetadataFileForWasmJsPublication - Generates the Gradle metadata file for publication 'wasmJs'.
generatePomFileForAndroidReleasePublication - Generates the Maven POM file for publication 'androidRelease'.
generatePomFileForIosArm64Publication - Generates the Maven POM file for publication 'iosArm64'.
generatePomFileForIosSimulatorArm64Publication - Generates the Maven POM file for publication 'iosSimulatorArm64'.
generatePomFileForIosX64Publication - Generates the Maven POM file for publication 'iosX64'.
generatePomFileForJvmPublication - Generates the Maven POM file for publication 'jvm'.
generatePomFileForKotlinMultiplatformPublication - Generates the Maven POM file for publication 'kotlinMultiplatform'.
generatePomFileForWasmJsPublication - Generates the Maven POM file for publication 'wasmJs'.
publish - Publishes all publications produced by this project.
publishAndroidReleasePublicationToMavenLocal - Publishes Maven publication 'androidRelease' to the local Maven repository.
publishIosArm64PublicationToMavenLocal - Publishes Maven publication 'iosArm64' to the local Maven repository.
publishIosSimulatorArm64PublicationToMavenLocal - Publishes Maven publication 'iosSimulatorArm64' to the local Maven repository.
publishIosX64PublicationToMavenLocal - Publishes Maven publication 'iosX64' to the local Maven repository.
publishJvmPublicationToMavenLocal - Publishes Maven publication 'jvm' to the local Maven repository.
publishKotlinMultiplatformPublicationToMavenLocal - Publishes Maven publication 'kotlinMultiplatform' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.
publishWasmJsPublicationToMavenLocal - Publishes Maven publication 'wasmJs' to the local Maven repository.

Notice the android release publication.

Testing by publishing locally

In the above output, you may have seen a publishToMavenLocal task. That's the task we'll use to test our publishing configuration. The maven local repository is just a directory on your system (usually ~/.m2/repository). Test by running this task:

./gradlew clean :segmented-display:publishToMavenLocal
Note

I included the clean task just to make sure that there were no lingering files on my system that got packaged. Generally, when publishing, it's a good idea to start from a clean state.

If you now inspect your ~/.m2/repository directory, however, you'll not find your nice group name and artifact name following the typical structure with a version. You'll actually just see a directory that has your root project name and a directory inside it with your module name. To remedy this, we should configure the group name and version. In your root build.gradle.kts file, add the following:

allprojects {
    group = "com.fsryan.ui"
    version = "0.0.3"
}
Note

Use your own group--not mine.

Note

I used version 0.0.3 because I had already published 0.0.1 and 0.0.2

Inspecting the output of publishing locally

Now run the publishToMavenLocal task again. You should see the group name and version in the directory structure of your local maven repository.

$ ls -1 ~/.m2/repository/com/fsryan/ui
segmented-display
segmented-display-android
segmented-display-iosarm64
segmented-display-iossimulatorarm64
segmented-display-iosx64
segmented-display-jvm
segmented-display-wasm-js
Note

The com/fsryan/ui part of the path came from the group name. The segmented-display part is the artifact name. In multiplatform publishing, the artifact that lacks a target indicator is the common artifact. The other artifacts are the platform-specific artifacts, as marked by the target indicator.

Let's also inspect one of those directories:

$ ls ~/.m2/repository/com/fsryan/ui/segmented-display-jvm/
0.0.3                    maven-metadata-local.xml

Here, we have a directory that contains the files for the version 0.0.3. If we were to publish another version, then there would be another directory with the version number.

Additionally, maven-metadata-local.xml is just an XML file that contains some metadata about segmented-display-jvm on the whole, describing things like when versions were published, etc. You don't really need to do anything with this file.

Note

In contrast to our local environment, this file will be called maven-metadata.xml when we publish to Maven Central.

Finally, let's look inside the 0.0.3 directory to see what it contains. These are the files that will get published to Maven Central.

$ ls -1 ~/.m2/repository/com/fsryan/ui/segmented-display-jvm/0.0.3
segmented-display-jvm-0.0.3-sources.jar
segmented-display-jvm-0.0.3.jar
segmented-display-jvm-0.0.3.module
segmented-display-jvm-0.0.3.pom

Notice that while the maven-publish plugin is automatically creating the sources jar, it's not automatically creating the javadoc.jar.

Adding the javadoc.jar file

To add the javadoc.jar file, we need to:

  1. Register a jar task that bundles the outputs of the dokkaHtml task into a jar file
  2. Add the output of the new jar task as an artifact of each MavenPublication
  3. Test whether we got things right by inspecting the output of the publishToMavenLocal task

Let's register the task that bundles the output of the dokkaHtml into a jar file. In segmented-display/build.gradle.kts:

tasks.register<Jar>("dokkaHtmlJar") {
    dependsOn(tasks.dokkaHtml)
    from(tasks.dokkaHtml.flatMap { it.outputDirectory })
    archiveClassifier.set("javadoc")
}

In line order, the dependsOn function sets the new dokkaHtmlJar task dependent upon the dokkaHtml task. The from function tells the dokkaHtmlJar task to include the output of the dokkaHtml task in the jar file. And finally, the archiveClassifier function sets the classifier of the jar file to "javadoc". It's important to set this archive classifier because consuming applications will know to look for this.

Now we'll add the output of dokkaHtmlJar as an artifact of each MavenPublication. If you go back to where we created the configureMultiplatformPublishing extension function on MavenPublication, add the following to the bottom of that function:

if (name != "androidRelease") {
    artifact(project.tasks.withType<Jar>().first { it.name == "dokkaHtmlJar" })
}
Note

I couldn't figure out why adding the javadoc jar to the androidRelease publication wasn't working. Having tried many things, I had to admit defeat and move on. Maven Central did not reject my library when I published it.

Rerun the segmented-display:publishToMavenLocal task and check the contents of segmented-display-jvm again. You should see that the javadoc.jar was added:

$ ls -1 ~/.m2/repository/com/fsryan/ui/segmented-display-jvm/0.0.3/
segmented-display-jvm-0.0.3-javadoc.jar
segmented-display-jvm-0.0.3-sources.jar
segmented-display-jvm-0.0.3.jar
segmented-display-jvm-0.0.3.module
segmented-display-jvm-0.0.3.pom

So you see now that the javadoc.jar got generated. Lets look at the files inside the javadoc.jar to ensure that our website was added:

$ jar -tf ~/.m2/repository/com/fsryan/ui/segmented-display-jvm/0.0.3/segmented-display-jvm-0.0.3-javadoc.jar
ryan[MyUILibrary] (main)$ jar -tf ~/.m2/repository/com/fsryan/ui/segmented-display-jvm/0.0.3/segmented-display-jvm-0.0.3-javadoc.jar
META-INF/
META-INF/MANIFEST.MF
segmented-display/
segmented-display/com.fsryan.ui.segments/
segmented-display/com.fsryan.ui.segments/create-symmetric-angled7-segment-ends-fun.html
segmented-display/com.fsryan.ui.segments/symmetric-even-angled-segment-ends.html
segmented-display/com.fsryan.ui.segments/index.html
segmented-display/com.fsryan.ui.segments/-rect7-segment-display.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends.html
segmented-display/com.fsryan.ui.segments/-classic7-segment-display.html
segmented-display/com.fsryan.ui.segments/draw-rect7-segment-char.html
segmented-display/com.fsryan.ui.segments/draw-classic7-segment-char.html
segmented-display/com.fsryan.ui.segments/-single-line-segmented-display.html
segmented-display/com.fsryan.ui.segments/transform-char-to-active-segments.html
segmented-display/com.fsryan.ui.segments/create-asymmetric-angled7-segment-ends-fun.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/outer-edge-left-area.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/right-intersection-pct.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/index.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/-companion/
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/-companion/index.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/-companion/-e-v-e-n.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/inner-edge-right-area.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/left-intersection-pct.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/inner-edge-left-area.html
segmented-display/com.fsryan.ui.segments/-angled-segment-ends/outer-edge-right-area.html
segmented-display/package-list
index.html
images/
images/copy-icon.svg
images/footer-go-to-link.svg
images/logo-icon.svg
images/nav-icons/
images/nav-icons/function.svg
images/nav-icons/interface.svg
images/nav-icons/enum.svg
images/nav-icons/typealias-kotlin.svg
images/nav-icons/field-value.svg
images/nav-icons/abstract-class.svg
images/nav-icons/class-kotlin.svg
images/nav-icons/class.svg
images/nav-icons/exception-class.svg
images/nav-icons/annotation-kotlin.svg
images/nav-icons/field-variable.svg
images/nav-icons/abstract-class-kotlin.svg
images/nav-icons/enum-kotlin.svg
images/nav-icons/object.svg
images/nav-icons/interface-kotlin.svg
images/nav-icons/annotation.svg
images/burger.svg
images/go-to-top-icon.svg
images/arrow_down.svg
images/copy-successful-icon.svg
images/homepage.svg
images/theme-toggle.svg
images/anchor-copy-button.svg
images/readme_headline.png
styles/
styles/font-jb-sans-auto.css
styles/main.css
styles/prism.css
styles/style.css
styles/logo-styles.css
scripts/
scripts/prism.js
scripts/platform-content-handler.js
scripts/symbol-parameters-wrapper_deferred.js
scripts/navigation-loader.js
scripts/main.js
scripts/clipboard.js
scripts/sourceset_dependencies.js
scripts/pages.json
navigation.html

You can see that the javadoc.jar contains the entire documentation site. This is what will be published to Maven Central and then picked up by javadoc.io.

Finally, let's inspect the contents of the segmented-display-jvm-0.0.3.pom file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.fsryan.ui</groupId>
  <artifactId>segmented-display-jvm</artifactId>
  <version>0.0.3</version>
  <description>jvm target of the Compose Multiplatform FS Ryan library for rendering segmented displays</description>
  <url>https://github.com/fsryan-org/segmented-display</url>
  <inceptionYear>2024</inceptionYear>
  <licenses>
    <license>
      <name>The Apache Software License, Version 2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
      <distribution>repo</distribution>
    </license>
  </licenses>
  <developers>
    <developer>
      <id>ryan</id>
      <name>Ryan Scott</name>
      <email>ryan@fsryan.com</email>
      <organization>FS Ryan</organization>
      <organizationUrl>https://www.fsryan.com</organizationUrl>
    </developer>
  </developers>
  <scm>
    <developerConnection>scm:git:git@github.com:fsryan-org/segmented-display.git</developerConnection>
    <url>https://github.com/fsryan-org/segmented-display.git</url>
  </scm>
  <issueManagement>
    <system>GitHub Issues</system>
    <url>https://github.com/fsryan-org/segmented-display/issues</url>
  </issueManagement>
  <dependencies>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib</artifactId>
      <version>2.0.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.compose.foundation</groupId>
      <artifactId>foundation-desktop</artifactId>
      <version>1.6.11</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.jetbrains.compose.ui</groupId>
      <artifactId>ui-desktop</artifactId>
      <version>1.6.11</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

As you can see, the full text of the pom file describes the project and its dependencies. This is what will be used by Maven Central to describe our project and direct users to our project source, etc. It's also what will be used by gradle to perform transitive dependency resolution. Notice that the properties we configured when we initially configured the maven publish plugin appear here as well.

Conclusion

In contrast to previous articles, this article was less exciting, but it was necessary. The additions to our project in this article makes our code more accessible to others while simultaneously adding the features that make our library more discoverable and usable. In the next article, we'll configure signing for our library and publish it to Maven Central. We'll then add some instructions to our README.md file about how to consume the library via binary dependency, and test it out ourselves.