Getting started — KMP

Add Justin to a Kotlin Multiplatform app targeting Android (API 24+) and iOS (16.0+). Five steps: point Gradle at the feed, add the dependency, wrap your app in AppTheme, render a component, verify.

1. Point Gradle at the feed

Justin is published to a private Azure DevOps Artifacts Maven feed as com.davidhorn.justin:shared. Add the feed in your consumer app’s settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        maven {
            name = "DavidhornJustin"
            url = uri("https://pkgs.dev.azure.com/<org>/<project>/_packaging/<feed>/maven/v1")
            credentials {
                username = "DavidhornJustin"
                password = providers.gradleProperty("adoToken").orNull
                    ?: error("Missing `adoToken` — add it to ~/.gradle/gradle.properties")
            }
            content {
                includeGroup("com.davidhorn.justin")
            }
        }
        google()
        mavenCentral()
    }
}

The concrete <org>/<project>/<feed> values are on the internal Justin Package Feeds page. The private feed is listed first and scoped to the com.davidhorn.justin group via content { includeGroup(...) }, so Gradle never asks it for public coordinates. Note the scoping is one-way: for the full dependency-confusion defence — a guarantee that google() and mavenCentral() never serve the com.davidhorn.justin group — declare the feed inside an exclusiveContent { } block instead.

Then put a Personal Access Token (Packaging: Read) in ~/.gradle/gradle.properties:

adoToken=<your-pat>

The username can be any non-empty string — ADO Artifacts uses HTTP Basic auth with the PAT as the password. (CI publishes to the same feed via Authorization: Bearer ... with an AAD OIDC access token; see .github/workflows/publish-kmp.yml. The feed accepts both schemes — Basic for PATs, Bearer for AAD tokens.)

2. Add the dependency

In your consumer app’s composeApp/build.gradle.kts:

commonMain.dependencies {
    implementation("com.davidhorn.justin:shared:<version>")
}

Replace <version> with the latest version from the feed. New releases are tagged kmp-v* in the source repo and published to the feed.

The artifact ships both Android (API 24+) and iOS (iosX64, iosArm64, iosSimulatorArm64) targets.

3. Wrap your app in AppTheme

AppTheme wraps Material 3’s MaterialTheme and injects Justin’s colors, typography, and corner radii. Do this once at the root of your composable tree.

import androidx.compose.runtime.Composable
import androidx.compose.foundation.isSystemInDarkTheme
import com.davidhorn.justin.theme.AppTheme

@Composable
fun App() {
    AppTheme(darkTheme = isSystemInDarkTheme()) {
        // your app content
    }
}

Everything below AppTheme has access to Justin tokens via MaterialTheme.colorScheme, AppSpacing, AppRadius, and AppTextStyles.

4. Use a component

import com.davidhorn.justin.components.button.DhPrimaryButton

@Composable
fun Submit(onSubmit: () -> Unit) {
    DhPrimaryButton(text = "Submit", onClick = onSubmit)
}

All Justin components are prefixed Dh, take their content first (here text and onClick) with an optional modifier after, and have sensible defaults so you rarely need more than one or two arguments. Buttons are split by role — DhPrimaryButton, DhSecondaryButton, DhTertiaryButton, DhErrorButton — with the rendering surface (filled, tonal, outlined, text) as a surface parameter.

5. Verify

Sync Gradle and run the app on either target:

  • A failed sync with a 401/Could not resolve com.davidhorn.justin:shared error means the feed credentials from step 1 aren’t in place.
  • The button renders with Justin’s primary color and rounded corners. A plain Material button means the composable isn’t inside AppTheme.

That’s the full setup. Everything below is optional.

Going further

Themes, tokens, and icons

  • Tokens: see Foundations for the full vocabulary. Access them in code as MaterialTheme.colorScheme.primary, AppSpacing.spacing4, AppRadius.md, AppTextStyles.textNormal().
  • Icons: the Justin Lucide pack is exposed via DhIcon — prefer DhIcon(DhIcons.<Name>, contentDescription = "…") for the curated surface, or DhIcon(Lucide.<PascalCase>, …) for any of the ~1,695 Lucide glyphs that aren’t curated. See the icon docs for the live catalog.
  • Dark mode: handled entirely by AppTheme; component code never hardcodes color.

See it running

The apps/kmp-demo/ app in the Justin repo is a minimal host showcasing every component as it lands:

bun run kmp:up       # builds + launches both Android and iOS targets
bun run kmp:smoke    # compile-only smoke test for the shared lib + demo

What’s next