JindongJindong

Core API

Using Jindong without Compose Runtime

Core API

Module: jindong-core | Package: io.github.compose.jindong.core

The Core API allows you to use Jindong haptic patterns without Compose Runtime dependency. This is useful for:

  • Non-Compose Kotlin Multiplatform projects
  • ViewModel or Repository layer haptic triggers
  • Background services or workers

Installation

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.compose-jindong:jindong-core:<version>")
        }
    }
}

Building Patterns

Use buildHapticPattern to create haptic patterns:

import io.github.compose.jindong.core.dsl.*
import io.github.compose.jindong.core.model.HapticIntensity
import io.github.compose.jindong.core.ms
 
val tapPattern = buildHapticPattern {
    haptic(50.ms)
}
 
val notificationPattern = buildHapticPattern {
    haptic(50.ms, HapticIntensity.MEDIUM)
    delay(50.ms)
    haptic(100.ms, HapticIntensity.STRONG)
}
 
val alertPattern = buildHapticPattern {
    repeat(3) {
        haptic(80.ms, HapticIntensity.HIGH)
        delay(50.ms)
    }
}

Transients, continuous feedback, and curves

The common DSL supports fixed intensity and sharpness, plus linear intensity and sharpness curves:

val rise = hapticCurve {
    point(0.ms, 0f)
    point(80.ms, 0.8f)
    point(300.ms, 0f)
}
val feedback = buildHapticPattern {
    repeat(3) {
        transient(HapticIntensity.STRONG, sharpness = 0.9f)
        delay(80.ms)
    }
    continuous(300.ms, intensityCurve = rise, sharpness = 0.6f)
}

This pattern contains impacts at 0ms, 80ms, and 160ms, followed by continuous feedback from 240ms through 540ms. Transients do not advance the logical timeline. Their physical pulse length depends on the backend. Continuous feedback requires a positive duration. Sharpness is normalized texture intent, not frequency in Hz.

A curve replaces its corresponding fixed value. Control points use event-relative time and absolute values in 0..1. Values and sharpness must be finite. Curves require at least two points, starting at 0ms and ending at the event duration. Rounded point times must strictly increase. A pattern permits at most 10,000 events and 100,000 control points.

Curves and events are immutable values. include, repetition, then, and Clip preserve their properties. scaleIntensity scales and clamps intensity curves. timeStretch scales their absolute times and rejects rounded duplicate points. reversed also reverses curves within their events.

Pattern Duration

HapticPattern.durationMs includes trailing silence. Builders pass the complete duration, and include and transforms preserve it. A delay-only pattern has no events and can have a positive duration.

val tick = buildHapticPattern {
    haptic(50.ms)
    delay(50.ms)
}
check(tick.durationMs == 100L)
val sameEventsWithMoreSilence = tick.copy(durationMs = 150L)

Equality includes events and duration. Events-only construction uses the latest event end as its default duration. Pass HapticPattern(events, durationMs = ...) when you construct a timeline with trailing silence.

Duration inputs must be finite and non-negative. They round to the nearest millisecond, with half-milliseconds rounded up. The shared Duration.toHapticMilliseconds() conversion applies that rule. Vibrations require at least 1ms; zero delays are allowed. See Migrating to 2.0 for source and ABI changes.

Executing Patterns

Use play() to execute and wait for completion:

import io.github.compose.jindong.core.play
 
suspend fun onButtonClick() {
    tapPattern.play()
}
 
// In a coroutine scope
lifecycleScope.launch {
    notificationPattern.play()
}

Fire-and-Forget

Use playAsync() when you don't need to wait:

import io.github.compose.jindong.core.playAsync
 
fun onButtonClick() {
    val handle = tapPattern.playAsync()
 
    // Optionally cancel later
    // handle.cancel()
}

Inline Execution

Use playHaptic for one-off patterns:

import io.github.compose.jindong.core.playHaptic
 
suspend fun onSuccess() {
    playHaptic {
        haptic(50.ms, HapticIntensity.MEDIUM)
        delay(50.ms)
        haptic(100.ms, HapticIntensity.STRONG)
    }
}

HapticManager

HapticManager is a singleton that manages haptic execution:

import io.github.compose.jindong.core.HapticManager
 
// Check device support
if (HapticManager.isSupported) {
    // Execute pattern
    HapticManager.executeAsync(pattern)
}
 
// Cancel ongoing haptic
HapticManager.cancel()
 
// Release resources (optional, auto-reinitializes on next use)
HapticManager.release()

Platform Initialization

Android

Automatic initialization via JindongInitializer ContentProvider. No manual setup needed.

For multi-process apps, call manually:

// In Application.onCreate()
HapticManager.initialize(applicationContext)

iOS

No initialization required.

DSL Reference

haptic

Creates a vibration event:

haptic(duration: Duration)
haptic(duration: Duration, intensity: HapticIntensity)
haptic(duration: Duration, intensity: HapticIntensity, iosParameters: IosHapticParameters?)

transient and continuous

fun transient(
    intensity: HapticIntensity = HapticIntensity.MEDIUM,
    sharpness: Float = 0.5f,
)
 
fun continuous(
    duration: Duration,
    intensity: HapticIntensity = HapticIntensity.MEDIUM,
    sharpness: Float = 0.5f,
    intensityCurve: HapticCurve? = null,
    sharpnessCurve: HapticCurve? = null,
)

Build reusable curves with hapticCurve { point(time: Duration, value: Float) } from io.github.compose.jindong.core.dsl. HapticCurve and HapticControlPoint are also available in io.github.compose.jindong.core.model for direct construction.

delay

Adds a pause between events:

delay(duration: Duration)

repeat

Repeats a block N times:

repeat(count: Int) {
    haptic(50.ms)
    delay(30.ms)
}

repeatWithIndex

Repeats with index access for dynamic patterns:

repeatWithIndex(5) { index ->
    val fade = HapticIntensity.Custom(1.0f - (index * 0.2f))
    haptic(50.ms, fade)
    delay(30.ms)
}

sequence

Groups elements (optional, for organization):

sequence {
    haptic(50.ms)
    delay(30.ms)
}

include

Includes an existing pattern:

val clickPattern = buildHapticPattern {
    haptic(50.ms)
}
 
val composedPattern = buildHapticPattern {
    include(clickPattern)
    delay(100.ms)
    include(clickPattern)
}

If included patterns overlap in time, they are not summed. A single vibration motor plays one amplitude at a time, so at each instant the strongest active event wins:

val composed = buildHapticPattern {
    include(strongPattern)  // e.g. 0-100ms at HIGH
    include(softPattern)    // e.g. 50-150ms at LIGHT
}
// 0-100ms plays at HIGH (it dominates the overlap), then 100-150ms plays at LIGHT.

Examples

ViewModel Usage

class MyViewModel : ViewModel() {
 
    private val successPattern = buildHapticPattern {
        haptic(50.ms, HapticIntensity.MEDIUM)
        delay(50.ms)
        haptic(100.ms, HapticIntensity.STRONG)
    }
 
    fun onAction() {
        viewModelScope.launch {
            // Do work...
            successPattern.play()
        }
    }
}

Repository Pattern

class HapticRepository {
 
    val tapFeedback = buildHapticPattern {
        haptic(50.ms, HapticIntensity.LIGHT)
    }
 
    val errorFeedback = buildHapticPattern {
        repeat(3) {
            haptic(80.ms, HapticIntensity.HIGH)
            delay(50.ms)
        }
    }
 
    suspend fun playTap() = tapFeedback.play()
    suspend fun playError() = errorFeedback.play()
}

Comparison with Compose API

FeatureCore APICompose API
Compose RuntimeNot requiredRequired
Trigger mechanismManual (play())Key-based (like LaunchedEffect)
Pattern definitionbuildHapticPattern {}Jindong {} composable
DSL styleRegular functions@Composable functions
Use caseViewModels, servicesUI components

See Also