Ink App Builder
Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)
- Skill
- Tool integration
- Software development
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The ink-app-builder skill provides a structured approach to implementing freehand drawing in Android applications using the androidx.ink library and Jetpack Compose. It focuses on the two-phase rendering model, utilizing InProgressStrokes for real-time wet ink capture and CanvasStrokeRenderer for displaying finalized dry ink strokes. The skill covers dependency configuration, brush management, geometry-based erasing, and stroke persistence using Room, ensuring a flicker-free drawing experience through synchronous state management.
What it does
- Configures Jetpack Ink dependencies and build settings for Android projects.
- Implements a dual-layer drawing surface using Compose Canvas and InProgressStrokes.
- Provides patterns for managing stock brushes like pressure pens, markers, and highlighters.
- Enables geometry-based stroke erasing and linear undo/redo history.
- Supports stroke persistence and retrieval using Room and Kotlin serialization.
- Delivers ViewModel and Compose implementations for state management.
When to use it
- When adding freehand drawing or stylus input to a Jetpack Compose Android app.
- When implementing a drawing surface that requires both wet and dry ink rendering.
- When managing stroke history, undo/redo functionality, or persistent storage.
- When configuring standard brush types and handling stroke geometry.
Advantages
- Provides a consistent architecture for handling wet-to-dry ink transitions.
- Ensures low-latency input processing through native Jetpack Ink integration.
- Includes specific patterns to prevent UI flickering during stroke finalization.
- Offers clear guidance on dependency management and build configuration.
Usage examples
- “How do I set up the dependencies for Jetpack Ink in my Android project?”
- The skill provides the necessary version catalog entries and build.gradle.kts configuration, including mandatory plugins for KSP and serialization.
- “How can I implement a drawing surface that supports both wet and dry ink?”
- The skill provides a complete DrawingSurface composable that stacks an InProgressStrokes layer over a Canvas, using a pendingStrokes buffer to prevent flickering.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/android/ink-app-builder/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip ink-app-builder.zip -d .agents/skills
Documentation by Google LLC
The Android Ink API (androidx.ink) is a modular Jetpack library for building
high-performance, low-latency freehand drawing experiences on Android. It uses a
wet ink and dry ink two-phase model:
Wet ink : Real-time, ultra-low-latency rendering while the user is drawing, handled by the
InProgressStrokescomposable.Dry ink : Finalized, immutable
Strokeobjects rendered by the app usingCanvasStrokeRenderer.
Setup and dependencies
The Ink API requires minSdk = 26 , which corresponds to Android 8.0 (API
level 26) and higher. All androidx.ink modules MUST share an identical version
in gradle/libs.versions.toml. Ask user permission before modifying project
build files.
Version selection (1.0.0 stable versus 1.1.0-alpha03 and higher)
Use
1.0.0(Stable) for production apps that rely onStockBrushesor pre-serialized.brushassets decoded withBrushFamily.decode. In1.0.0stable,BrushFamily.encode,BrushFamily.decode,AndroidBrushFamilySerialization, andBrushFamilyDecodeCallbackrequire@OptIn(ExperimentalInkCustomBrushApi::class).Use
1.1.0-alpha03and higher when the app constructs programmatic custom brushes (BrushFamily,BrushCoat,BrushTip,BrushPaint,BrushBehavior) in Kotlin code. Starting in1.1.0-alpha02and higher,BrushFamilyserialization requires no@OptIn(and in1.1.0-alpha05and higher,ExperimentalInkCustomBrushApiis@RestrictTo(LIBRARY_GROUP), so NEVER annotate1.1.0-alpha03and higher code with it).
Version catalog (gradle/libs.versions.toml)
[versions]
ink = "<latest_version>"
inputMotionPrediction = "<latest_version>"
[libraries]
# Core Ink modules
androidx-ink-authoring = { module = "androidx.ink:ink-authoring", version.ref = "ink" }
androidx-ink-brush = { module = "androidx.ink:ink-brush", version.ref = "ink" }
androidx-ink-geometry = { module = "androidx.ink:ink-geometry", version.ref = "ink" }
androidx-ink-nativeloader = { module = "androidx.ink:ink-nativeloader", version.ref = "ink" }
androidx-ink-rendering = { module = "androidx.ink:ink-rendering", version.ref = "ink" }
androidx-ink-strokes = { module = "androidx.ink:ink-strokes", version.ref = "ink" }
androidx-ink-storage = { module = "androidx.ink:ink-storage", version.ref = "ink" }
# Compose integration modules
androidx-ink-authoring-android = { module = "androidx.ink:ink-authoring-android", version.ref = "ink" }
androidx-ink-authoring-compose = { module = "androidx.ink:ink-authoring-compose", version.ref = "ink" }
androidx-ink-brush-compose = { module = "androidx.ink:ink-brush-compose", version.ref = "ink" }
androidx-ink-geometry-compose = { module = "androidx.ink:ink-geometry-compose", version.ref = "ink" }
# Motion prediction (required for low-latency stylus tracking)
androidx-input-motionprediction = { module = "androidx.input:input-motionprediction", version.ref = "inputMotionPrediction" }
# Persistence (kotlinx.serialization + Room with KSP)
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }App module configuration (app/build.gradle.kts)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp) // Mandatory when using Room in Kotlin
}
android {
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.ink.authoring)
implementation(libs.androidx.ink.brush)
implementation(libs.androidx.ink.geometry)
implementation(libs.androidx.ink.nativeloader)
implementation(libs.androidx.ink.rendering)
implementation(libs.androidx.ink.strokes)
implementation(libs.androidx.ink.storage)
implementation(libs.androidx.ink.authoring.android)
implementation(libs.androidx.ink.authoring.compose)
implementation(libs.androidx.ink.brush.compose)
implementation(libs.androidx.ink.geometry.compose)
implementation(libs.androidx.input.motionprediction)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) // NEVER use annotationProcessor in Kotlin
}Core drawing surface and default brush
Create a standard pressure-sensitive pen Brush with
Brush.createWithComposeColor (from ink-brush-compose). The epsilon
parameter defines the minimum distance between two input points, in stroke-space
units, to be considered visually distinct (0.1f for general drawing, 0.01f
for fine precision tools; Brush enforces 0f < epsilon <= size):
/**
* Creates a default pressure-sensitive pen [Brush].
*
* The `epsilon` parameter sets the geometric tolerance (minimum point distance
* in stroke units) controlling how far a stroke can be zoomed before triangle
* artifacts appear (`0f < epsilon <= size`).
*/
fun createDefaultPenBrush(): Brush {
return Brush.createWithComposeColor(
family = StockBrushes.pressurePen(),
color = Color.Black,
size = 5f,
epsilon = 0.1f,
)
}<br />
Stack two layers inside a Box for the drawing surface: a Compose Canvas that
renders finalized dry Stroke objects inside nativeCanvas.withSave using
CanvasStrokeRenderer, and an InProgressStrokes composable on top for
low-latency wet ink capture. Encapsulate a synchronous pendingStrokes Compose
Snapshot State buffer inside DrawingSurface so the dry Canvas invalidates in
the exact same UI thread run loop when onStrokesFinished fires, preventing a
1-frame wet-to-dry flicker:
@Composable
fun DrawingSurface(
strokes: List<Stroke>,
canvasStrokeRenderer: CanvasStrokeRenderer,
currentBrush: Brush,
onGetNextBrush: () -> Brush,
onStrokesFinished: (List<Stroke>) -> Unit,
isEraserMode: Boolean,
modifier: Modifier = Modifier,
textureStore: AppTextureBitmapStore = LocalTextureStore.current,
onErase: (Float, Float) -> Unit = { _, _ -> },
onEraseStart: () -> Unit = {},
onEraseEnd: () -> Unit = {},
) {
val cacheGen by textureStore.generation.collectAsStateWithLifecycle()
// Holds newly finished strokes synchronously for the single frame before
// asynchronous StateFlow collection delivers the updated strokes list.
var pendingStrokes by remember { mutableStateOf<List<Stroke>>(emptyList()) }
SideEffect {
if (pendingStrokes.isNotEmpty()) {
pendingStrokes = if (strokes.isEmpty()) {
emptyList()
} else {
pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
}
val strokesToDraw = remember(strokes, pendingStrokes) {
if (pendingStrokes.isEmpty()) {
strokes
} else {
strokes + pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
val identityMatrix = remember { Matrix() }
val currentOnErase by rememberUpdatedState(onErase)
val currentOnEraseStart by rememberUpdatedState(onEraseStart)
val currentOnEraseEnd by rememberUpdatedState(onEraseEnd)
Box(modifier = modifier) {
// Layer 1: Dry ink - completed strokes + same-frame pending handoff strokes
Canvas(modifier = Modifier.fillMaxSize()) {
val nativeCanvas = drawContext.canvas.nativeCanvas
strokesToDraw.fastForEach { stroke ->
nativeCanvas.withSave {
canvasStrokeRenderer.draw(
canvas = this,
stroke = stroke,
strokeToScreenTransform = identityMatrix,
)
}
}
}
// Layer 2: Eraser touch handler OR Wet ink (InProgressStrokes)
if (isEraserMode) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { currentOnEraseStart() },
onDragEnd = { currentOnEraseEnd() },
onDragCancel = { currentOnEraseEnd() },
onDrag = { change, _ ->
currentOnErase(change.position.x, change.position.y)
change.consume()
},
)
}
)
} else {
key(cacheGen) {
InProgressStrokes(
defaultBrush = currentBrush,
nextBrush = onGetNextBrush,
onStrokesFinished = { newStrokes ->
pendingStrokes = pendingStrokes + newStrokes
onStrokesFinished(newStrokes)
},
textureBitmapStore = textureStore,
)
}
}
}
}<br />
Workflow and task routing
Follow these conditional instructions based on the requested task:
Step 1 (Dependencies and SDK setup) : When adding Ink API dependencies or configuring
minSdk, follow the setup blocks in this file and the Ink API setup guide. Verify thatink-nativeloader,ink-authoring-android, andandroidx.input:input-motionpredictionare present.Step 2 (Drawing surface and stroke rendering) : When creating a Compose drawing screen with wet ink (
InProgressStrokes) and dry ink (CanvasStrokeRenderer), implementDrawingSurfacefrom this file and review Draw a stroke. ForInProgressStrokesparameter details,BlendMode.Multiplyhighlighter layer compositing, andTextureBitmapStoregeneration invalidation, you MUST follow drawing-and-brushes.md.Step 3 (Stock brushes and brush switching) : When creating, modifying, or switching stock brushes (
StockBrushes.pressurePen,marker,highlighter,dashedLine, oremojiHighlighter), review Brush APIs and you MUST follow drawing-and-brushes.md forepsilongeometry constraints, highlighter alpha preservation, and Android 14 (API level 34)emojiHighlighterrules.Step 4 (Eraser, undo and redo, and MVVM architecture) : When implementing a geometry-based stroke eraser, linear undo and redo history stacks, or MVVM
ViewModeland Hilt wiring, review Geometry APIs and you MUST follow editing-and-architecture.md.Step 5 (Persistence, bitmap export, and gesture coexistence) : When saving and loading strokes with Room (
StrokeInputBatch.encodeanddecode), preserving UI state across configuration changes, exporting drawings to aBitmapor PNG file, or combining long-press drag gestures with drawing (pointerInputWithSiblingFallthrough), review State preservation and persistent storage and you MUST follow persistence-and-advanced.md.Programmatic custom brushes : For custom
BrushFamilycreation, custom tip shapes, or behavior node graphs, suggest theink-custom-brush-builderskill.
Antipatterns
NEVER use
InProgressStrokesViewin Compose screens ---that's the View-based API. Use theInProgressStrokescomposable instead.NEVER omit
ink-nativeloaderorink-authoring-android---missingink-nativeloaderthrowsUnsatisfiedLinkError, and missingink-authoring-androidthrowsClassNotFoundExceptionat runtime.NEVER use
View.OnTouchListenerorMotionEventdirectly ---theInProgressStrokescomposable captures and processes touch and stylus input internally.NEVER draw strokes manually with
Canvas.drawPathor by iterating stroke points---always callCanvasStrokeRenderer.draw(canvas, stroke, transform).NEVER call
CanvasStrokeRenderer.create(forcePathRendering = true)---the 2-argumentforcePathRenderingoverload is@RestrictTo(LIBRARY_GROUP). Use the publicCanvasStrokeRenderer.create(textureStore)factory, which automatically detects softwareBitmapcanvases and falls back to path rendering.NEVER rely solely on asynchronous
StateFlowcollection inonStrokesFinished---InProgressStrokesremoves completed wet strokes immediately afteronStrokesFinishedreturns on the UI thread, whilecollectAsStateWithLifecycleupdates on a later run loop, causing a 1-frame wet-to-dry flicker. Always mutate Compose Snapshot State synchronously in the same UI thread run loop using apendingStrokesbuffer as shown inDrawingSurface.NEVER store strokes as bitmaps for undo, redo, or database persistence---store
Strokeobjects in memory and serializeStrokeInputBatchbytes with brush metadata for disk storage.NEVER use
annotationProcessorfor Room in Kotlin ---always apply thecom.google.devtools.kspplugin and useksp(libs.androidx.room.compiler)to avoidAppDatabase_Impl does not existcrashes at launch.
Files
- ATTRIBUTION.mdAdded by AutoGineer982 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md15.3 KiB
- references/android/develop/ui/compose/touch-input/stylus-input/ink-api-brush-apis.md5.0 KiB
- references/android/develop/ui/compose/touch-input/stylus-input/ink-api-draw-stroke.md2.1 KiB
- references/android/develop/ui/compose/touch-input/stylus-input/ink-api-geometry-apis.md1.2 KiB
- references/android/develop/ui/compose/touch-input/stylus-input/ink-api-setup.md1.2 KiB
- references/android/develop/ui/compose/touch-input/stylus-input/ink-api-state-preservation.md11.9 KiB
- references/drawing-and-brushes.md9.0 KiB
- references/editing-and-architecture.md10.2 KiB
- references/persistence-and-advanced.md10.6 KiB
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Google LLC
- Publisher
- Android (Google) (Official)
- Distributed by
- Android (Google)
- Source
- android/skills · jetpack-compose/ink/ink-app-builder · commit bf68e2d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.txt)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download ink-app-builder.zip