Skip to content

Android ​

LoaderKit for Android draws indicators with Canvas. It has two artifacts:

  • loaderkit-core: the spec model, parser and validator, the evaluator, the built-in indicators and LoaderKitView.
  • loaderkit-compose: the LoaderKitIndicator composable. It includes loaderkit-core.

Requires minSdk 24.

Install ​

Both artifacts are on Maven Central.

kotlin
// build.gradle.kts
dependencies {
    implementation("io.github.maitrungduc1410:loaderkit-core:<version>")
    // Jetpack Compose (includes loaderkit-core):
    implementation("io.github.maitrungduc1410:loaderkit-compose:<version>")
}

Make sure mavenCentral() is in the repositories of your settings.gradle.kts. Replace <version> with the latest release from the GitHub releases.

XML ​

xml
<io.github.maitrungduc1410.loaderkit.LoaderKitView
    android:id="@+id/loader"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:indicator="BallSpinFadeLoader"
    app:indicatorColor="?attr/colorPrimary"
    app:speed="1.5"
    app:hidesWhenStopped="false" />
AttributeFormatMeaning
app:indicatorstringName of a built-in indicator
app:indicatorColorcolor or referenceColor of every element
app:speedfloatPlayback rate, 1 is the speed of the spec
app:hidesWhenStoppedbooleanDraw nothing while stopped
app:cycleProgressfloatA frozen point of the animation cycle, 0 to 1

app:indicatorColor uses the same format as the Material Components attribute of the same name, so both libraries can be used together.

With wrap_content the view is 40dp square, plus padding. The indicator is drawn in the largest centered square that fits inside the padding.

Kotlin ​

kotlin
import io.github.maitrungduc1410.loaderkit.LoaderKitView

val loader = LoaderKitView(context).apply {
    indicator = "BallPulse"
    params = mapOf("count" to 5.0)
    color = Color.MAGENTA
    // or one color per element, cycling: colors = intArrayOf(Color.RED, Color.GREEN, Color.BLUE)
    onError = { error -> Log.w("Loader", error.message.orEmpty()) }
}

loader.stop()
loader.start()

Properties ​

PropertyTypeDefaultMeaning
indicatorString?"BallPulse"Built-in to draw when no spec is set
specIndicatorSpec?nullA custom spec. Wins over indicator
setSpecJson(json)functionParses and sets a JSON spec. Errors go to onError
paramsMap<String, Double>emptyParam overrides. Unknown names are ignored
colorInttheme android:colorForegroundColor of every element
colorsIntArray?nullElement i uses colors[i % size]
speedDouble1.0Playback rate. 0 or less pauses
isAnimatingBooleantrueAlso start() and stop(). Stopping freezes the frame
hidesWhenStoppedBooleantrueDraw nothing while stopped
cycleProgressDouble?nullA frozen point of the cycle in [0, 1]. null follows the clock
respectsReduceMotionBooleantrueShow a still frame when the system turns animations off
onError((InvalidIndicatorSpecException) -> Unit)?nullCalled when a name or spec cannot be drawn. Logged when not set

Every property can be set on its own and in any order. reset() restores all of them except onError, which makes the view easy to recycle in a RecyclerView or a React Native view manager. The clock pauses while the view is detached or hidden.

Jetpack Compose ​

kotlin
import io.github.maitrungduc1410.loaderkit.compose.LoaderKitIndicator

LoaderKitIndicator(
    indicator = "BallPulse",
    modifier = Modifier.size(64.dp),
    params = mapOf("count" to 4.0),
    color = MaterialTheme.colorScheme.primary,
    speed = 1.0,
    animating = isLoading,
)
ParameterTypeDefault
indicator or specString or IndicatorSpecrequired (two overloads)
modifierModifierModifier, 40.dp square unless it sets a size
paramsMap<String, Double>empty
colorColorblack, or white when the system is in dark mode
colorsList<Color>?null
speedDouble1.0
animatingBooleantrue
cycleProgressDouble?null
respectsReduceMotionBooleantrue

The composable has no hidesWhenStopped. To hide it, leave it out of the composition. An unknown name or an invalid spec draws nothing and logs a warning.

Progress indicators ​

LoaderKitProgress shows how much of a task is done: 50 designs across 10 types, determinate or indeterminate, with smooth value changes.

xml
<io.github.maitrungduc1410.loaderkit.LoaderKitProgressView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:progressType="linear"
    app:progressVariant="wavy"
    app:progressValue="0.4" />
kotlin
progress.value = 0.8        // glides to 0.8; null is indeterminate
progress.smooth = false     // draw every value as it comes

// Compose
LoaderKitProgress(value = progress, type = ProgressType.Gauge, showLabel = true, size = 64.dp)

See Progress indicators for every type, variant and option.

Custom specs ​

Experimental

Writing your own spec is experimental: until the schema is declared stable, a minor release may change it. Built-in indicators are not affected.

Parse a JSON spec with IndicatorSpec.parse. It validates the spec and throws InvalidIndicatorSpecException, whose errors lists every problem.

kotlin
import io.github.maitrungduc1410.loaderkit.IndicatorSpec
import io.github.maitrungduc1410.loaderkit.InvalidIndicatorSpecException

val json = context.assets.open("typing-dots.json").bufferedReader().use { it.readText() }

try {
    val spec = IndicatorSpec.parse(json)
    loader.spec = spec
} catch (e: InvalidIndicatorSpecException) {
    e.errors.forEach { Log.w("Loader", it) }
}

// Or let the view parse it: errors go to onError and nothing is drawn.
loader.setSpecJson(json)

// Compose
LoaderKitIndicator(spec = spec, params = mapOf("count" to 4.0))

IndicatorSpec.validate(json) returns the same list of problems without throwing. The view never throws for bad input.

In a Kotlin string literal, write "${'$'}param" for the $param key, or keep specs in asset files.

Evaluating without a view ​

The evaluator is plain Kotlin, so you can sample a spec without drawing it, for example in a test:

kotlin
import io.github.maitrungduc1410.loaderkit.ElementState
import io.github.maitrungduc1410.loaderkit.evaluate
import io.github.maitrungduc1410.loaderkit.timeForCycleProgress

val states: List<ElementState> = evaluate(spec, t = 0.4, params = mapOf("count" to 5.0))
val frozenTime = timeForCycleProgress(spec, cycleProgress = 0.5)

See also ​

Released under the MIT License.