Skip to content

Kotlin Multiplatform SDK

The Pulsar Kotlin Multiplatform SDK exposes the same building blocks as the native SDKs — Presets, PatternComposer, and RealtimeComposer — from common Kotlin code. The library targets Android and iOS (iosX64, iosArm64, iosSimulatorArm64) and bridges to the native Pulsar implementation on each platform.

  • Kotlin 2.0+
  • Android API 24+ (Android 7.0) for Android targets
  • iOS 13+ for iOS targets

Latest available version: 0.0.2

Add Pulsar KMP as a Gradle dependency in your shared module:

dependencies {
implementation("com.swmansion:pulsar-kmp:0.0.2")
}

The library publishes the artifact as a Kotlin Multiplatform module — Gradle will resolve the right variant for each target automatically.

Pulsar registers itself on Android through a ContentProvider (PulsarInitializer), so no manual wiring is required.

Declare the vibration permission in your Android app’s AndroidManifest.xml:

<manifest ...>
<uses-permission android:name="android.permission.VIBRATE" />
<application ...>
...
</application>
</manifest>

Without android.permission.VIBRATE, Android will block vibration playback. Pulsar logs a warning and skips the vibration instead of crashing.

The iOS target uses CoreHaptics directly — no extra setup beyond adding the dependency. When consuming the KMP library from an Xcode project, link the produced framework as you would any other Kotlin/Native binary.

Create a Pulsar instance from common code:

import com.swmansion.pulsar.kmp.Pulsar
val pulsar = Pulsar.create()

Pulsar.create() resolves the platform implementation registered for the current target. On Android the factory is registered automatically by PulsarInitializer; on iOS the factory is provided by the iOS target. If you need to register a custom factory (for tests or advanced setups), use Pulsar.registerFactory(...) before the first create() call.


A collection of ready-to-use haptic patterns accessed through pulsar.getPresets().

val presets = pulsar.getPresets()

Each built-in preset is exposed as a direct method on the PulsarPresets instance, mirroring the React Native API.

val presets = pulsar.getPresets()
presets.hammer()
presets.dogBark()
presets.fanfare()

Built-in presets can also be played by passing their name to play(). The method returns true if the preset was found and dispatched, or false otherwise.

fun play(name: String): Boolean

getByName(name) returns a lightweight PulsarPreset handle that can be played later, or null if the name is unknown:

fun getByName(name: String): PulsarPreset?

Available names:

  • Afterglow
  • Aftershock
  • Alarm
  • Anvil
  • Applause
  • Ascent
  • BalloonPop
  • Barrage

Example

val presets = pulsar.getPresets()
presets.play("Hammer")
presets.play("DogBark")

For per-preset descriptions, see the iOS and Android reference pages — the underlying patterns are shared.

PulsarPresets exposes its own preload/cache controls in addition to the equivalents on the Pulsar instance.

MethodDescription
preloadPresetByName(name: String)Preload a single preset by name
preloadPresetByNames(names: List<String>)Preload multiple presets in a batch
enableCache(state: Boolean)Enable or disable preset caching
isCacheEnabled(): BooleanReturns true if caching is currently enabled
resetCache()Drop all cached preset data

Cross-platform system feedback styles are exposed as direct methods so you can call them from common code without name lookup.

MethodDescription
systemImpactHeavy()Heavy impact
systemImpactLight()Light impact
systemImpactMedium()Medium impact
systemImpactRigid()Rigid impact
systemImpactSoft()Soft impact

Example

val presets = pulsar.getPresets()
presets.systemImpactMedium()
presets.systemNotificationSuccess()

Android’s HapticFeedbackConstants and VibrationEffect system styles are also surfaced on the common API. They are no-ops on iOS, so you can call them from common code — but they only produce feedback on Android.

MethodDescription
systemEffectClick()VibrationEffect.EFFECT_CLICK
systemEffectDoubleClick()VibrationEffect.EFFECT_DOUBLE_CLICK
systemEffectTick()VibrationEffect.EFFECT_TICK
systemEffectHeavyClick()VibrationEffect.EFFECT_HEAVY_CLICK
systemLongPress()HapticFeedbackConstants.LONG_PRESS

Composes and plays custom haptic patterns. Get a new instance from pulsar.getPatternComposer(). Each call returns a fresh composer, so you can manage multiple patterns independently.

val composer = pulsar.getPatternComposer()

Parses a PatternData object and prepares it for playback.

fun parsePattern(pattern: PatternData)

Convenience method that parses and immediately plays a pattern.

fun playPattern(pattern: PatternData)

Plays the previously parsed pattern.

fun play()

Plays only the audio simulation of the parsed pattern, skipping the haptic engine. Useful for previewing patterns on simulators.

fun playAudioOnly()

Stops all playback.

fun stop()
val composer = pulsar.getPatternComposer()
val pattern = PatternData(
continuousPattern = ContinuousPattern(
amplitude = listOf(
ValuePoint(time = 0, value = 0f),
ValuePoint(time = 200, value = 1f),
ValuePoint(time = 400, value = 0f),
),
frequency = listOf(
ValuePoint(time = 0, value = 0.3f),
ValuePoint(time = 400, value = 0.8f),
)
),
discretePattern = listOf(
ConfigPoint(time = 0, amplitude = 1f, frequency = 0.5f),
ConfigPoint(time = 100, amplitude = 0.5f, frequency = 0.5f),
)
)
composer.playPattern(pattern)

Provides real-time haptic control with live amplitude and frequency modulation. Useful for gesture-driven or continuously evolving haptic experiences. Get a composer from pulsar.getRealtimeComposer().

val realtime = pulsar.getRealtimeComposer()

On Android, you can also request a composer that uses a specific strategy, or read/write the global default via the realtimeComposerStrategy property on Pulsar. See RealtimeComposerStrategy for available strategies.

val realtime = pulsar.getRealtimeComposer(RealtimeComposerStrategy.PRIMITIVE_COMPLEX)
pulsar.realtimeComposerStrategy = RealtimeComposerStrategy.ENVELOPE

Updates the ongoing haptic with new amplitude and frequency values. Automatically starts playback if it is not already active. Values should be in the 0-1 range.

fun set(amplitude: Float, frequency: Float)

Plays a single discrete haptic event.

fun playDiscrete(amplitude: Float, frequency: Float)

Stops the active continuous haptic.

fun stop()

Returns true if a continuous haptic is currently playing.

fun isActive(): Boolean
val realtime = pulsar.getRealtimeComposer()
realtime.set(amplitude = 0.5f, frequency = 0.8f)
realtime.set(amplitude = 1.0f, frequency = 0.3f)
realtime.playDiscrete(amplitude = 0.7f, frequency = 0.5f)
realtime.stop()

Plays haptics from a cross-platform AdaptivePreset. The correct configuration is selected for the current platform automatically. Each platform can provide either a custom PatternData or a callback that triggers a built-in or system preset directly.

val haptics = pulsar.createAdaptiveHaptics(preset)
sealed class AdaptivePresetConfig {
class Function(val play: (PulsarPresets) -> Unit) : AdaptivePresetConfig()
class Pattern(val pattern: PatternData) : AdaptivePresetConfig()
}
  • Function — a callback that receives the PulsarPresets instance and triggers a native or built-in preset directly.
  • Pattern — a custom PatternData parsed once on creation and replayed on each play().
data class AdaptivePreset(
val ios: AdaptivePresetConfig,
val android: AdaptivePresetConfig,
)

Plays the configuration for the current platform. If the platform config is a Function, it is invoked. If it is a Pattern, it is played via the underlying pattern composer.

Stops the active pattern. No-op when the platform config is a Function.

import com.swmansion.pulsar.kmp.AdaptiveHaptics
import com.swmansion.pulsar.kmp.AdaptivePreset
import com.swmansion.pulsar.kmp.AdaptivePresetConfig
import com.swmansion.pulsar.kmp.ConfigPoint
import com.swmansion.pulsar.kmp.ContinuousPattern
import com.swmansion.pulsar.kmp.PatternData
import com.swmansion.pulsar.kmp.Pulsar
val pulsar = Pulsar.create()
val adaptivePreset = AdaptivePreset(
ios = AdaptivePresetConfig.Function { presets ->
presets.systemNotificationSuccess()
},
android = AdaptivePresetConfig.Pattern(
PatternData(
continuousPattern = ContinuousPattern(amplitude = emptyList(), frequency = emptyList()),
discretePattern = listOf(
ConfigPoint(time = 0, amplitude = 1f, frequency = 0.5f),
ConfigPoint(time = 150, amplitude = 0.6f, frequency = 0.4f),
),
),
),
)
val haptics = pulsar.createAdaptiveHaptics(adaptivePreset)
haptics.play()

Configuration methods available directly on the Pulsar instance.

MethodDescription
enableHaptics(state: Boolean)Enable or disable all haptic feedback
enableSound(state: Boolean)Enable or disable audio simulation
enableCache(state: Boolean)Enable or disable preset caching
isCacheEnabled()Returns true if preset caching is enabled
clearCache()Clear the preset cache
preloadPresets(presetNames: List<String>)Preload presets by name for faster playback
stopHaptics()Stop all currently playing haptics
shutDownEngine()Tear down the underlying haptic engine
isHapticsEnabled()Returns true if haptics are currently enabled
isHapticsSupported()Returns true if the device supports haptics
canPlayHaptics()Returns true when haptics can play right now
hapticSupport(): CompatibilityModeReturns the device’s CompatibilityMode
forceHapticsSupportLevel(mode: CompatibilityMode)(Android only) Override the detected support level. Intended for testing fallback behavior.
enableImpulseCompositionMode(state: Boolean)(Android only) Enable or disable impulse composition mode
realtimeComposerStrategy (property)(Android only) Get or set the default RealtimeComposerStrategy
val pulsar = Pulsar.create()
pulsar.preloadPresets(listOf("Hammer", "DogBark"))
if (pulsar.canPlayHaptics()) {
pulsar.getPresets().play("Hammer")
}
pulsar.enableHaptics(false)

All shared types live in the com.swmansion.pulsar.kmp package and are usable from common code.

Describes a complete haptic pattern with discrete pulses and continuous envelope curves.

data class PatternData(
val continuousPattern: ContinuousPattern,
val discretePattern: List<ConfigPoint>
)

A secondary constructor accepts the raw triplet form (List<List<List<Float>>> for continuous, List<List<Float>> for discrete) for parity with serialized pattern definitions.

Represents continuous haptic curves for amplitude and frequency.

data class ContinuousPattern(
val amplitude: List<ValuePoint>,
val frequency: List<ValuePoint>
)

A single point in a continuous curve.

data class ValuePoint(
val time: Long, // Milliseconds from pattern start
val value: Float // Normalized value (0-1)
)

A single discrete haptic event.

data class ConfigPoint(
val time: Long, // Milliseconds from pattern start
val amplitude: Float, // Intensity (0-1)
val frequency: Float // Sharpness (0-1)
)

Use discretePattern for distinct taps and impacts. Use continuousPattern envelopes to shape a sustained haptic over time.

The haptic capability level of the current device, returned by pulsar.hapticSupport().

enum class CompatibilityMode {
NO_SUPPORT,
LIMITED_SUPPORT,
STANDARD_SUPPORT,
ADVANCED_SUPPORT,
}

On Android, pulsar.forceHapticsSupportLevel(mode) accepts these values to override the detected fallback path.

Example

val pulsar = Pulsar.create()
if (pulsar.hapticSupport() >= CompatibilityMode.STANDARD_SUPPORT) {
pulsar.getPresets().dogBark()
}

Android-only. Controls how RealtimeComposer simulates continuous haptics, since Android has no native continuous haptic API. Pass one of these values to pulsar.getRealtimeComposer(strategy) or assign to pulsar.realtimeComposerStrategy.

enum class RealtimeComposerStrategy {
ENVELOPE,
PRIMITIVE_TICK,
PRIMITIVE_COMPLEX,
ENVELOPE_WITH_DISCRETE_PRIMITIVES,
}
ValueDescription
ENVELOPEApproximation based on the Envelope API. Allows control over amplitude and frequency, but the signal oscillates and can be unstable. Available on Android API 36+.
PRIMITIVE_TICKApproximation using the Composition API TICK primitive at varying intervals. Amplitude is controllable; frequency is simulated by the timing between ticks. Signal is discrete rather than continuous.
PRIMITIVE_COMPLEXSimilar to PRIMITIVE_TICK, but uses multiple primitives depending on the requested frequency.
ENVELOPE_WITH_DISCRETE_PRIMITIVESDefault. Hybrid strategy. Uses the Envelope API for continuous events (API 36+) and composition primitives for discrete events (API 33+). Best of both worlds when both event types are used.

Used internally by curve interpolation and exposed for advanced custom composers.

data class ControlPoint(
val intensity: Float,
val sharpness: Float,
val duration: Long
)

For tests or hosting environments where the default initializer cannot run, register a custom factory before the first Pulsar.create() call:

import com.swmansion.pulsar.kmp.Pulsar
import com.swmansion.pulsar.kmp.PulsarPlatformFactory
Pulsar.registerFactory(myFactory)
val pulsar = Pulsar.create()

PulsarPlatformFactory produces a PulsarPlatformHandle, which exposes the low-level operations (presets(), patternComposer(), realtimeComposer(), lifecycle, capability checks) that the public API delegates to.