Getting started — KMP

Add Justin to a Kotlin Multiplatform app. Targets Android (API 24+) and iOS (16.0+).

Install

Justin is published to a private Azure DevOps Artifacts Maven feed as com.davidhorn.justin:shared. Point Gradle at 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 private feed is listed first and scoped to the com.davidhorn.justin group via content { includeGroup(...) } so Gradle never tries to resolve public coordinates from it (and vice versa) — defence against dependency-confusion squatting.

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.)

Then 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 (current at the time of writing: 0.2.2). 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.

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.

Use a component

import com.davidhorn.justin.components.button.DhButton

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

All Justin components are prefixed Dh, accept a modifier: Modifier = Modifier first parameter, and have sensible defaults so you rarely need more than one or two arguments.

Themes, tokens, and icons

  • Tokens: see Foundations for the full vocabulary. Access them in code as MaterialTheme.colorScheme.primary, AppSpacing.spacing4, AppRadius.md, AppTextStyles.bodyLarge.
  • Icons: the Davidhorn icon set is exposed via Res.drawable.ic_*. Prefer these over Material Icons.
  • Dark mode: handled entirely by AppTheme; component code never hardcodes color.

See it running

The apps/kmp-demo/ app 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