Shortcut

Project Url: MehdiKh93/Shortcut
Introduction: A simple library to add dynamic and pinned shortcuts
More: Author   ReportBugs   
Tags:

Maven Central CI License

A small Kotlin DSL for dynamic and pinned Android app shortcuts, built on ShortcutManagerCompat.

API documentation · Changelog · Contributing

Requirements

  • minSdk 23 (Android 6.0, required by current AndroidX). Dynamic shortcuts appear on API 25+, pinned shortcuts on API 26+ with a legacy-launcher fallback below that — the library handles the version checks internally via ShortcutManagerCompat, so no Build.VERSION checks are needed in your code.
  • AndroidX. The library depends on androidx.core; apps still on the legacy support libraries must migrate to AndroidX first.
  • Kotlin. The 2.0 API is a Kotlin DSL designed for Kotlin callers.

Building from source

The project builds with the Gradle wrapper (Gradle 9.6, AGP 9.3, built-in Kotlin) and needs JDK 17+ and an Android SDK with platform 36:

./gradlew build

Add the dependency

The library is published on Maven Central as io.github.mehdikhalifeh:shortcut-core (no extra repository configuration needed):

dependencies {
    implementation("io.github.mehdikhalifeh:shortcut-core:2.0.0")

    // Optional Jetpack Compose bindings:
    implementation("io.github.mehdikhalifeh:shortcut-compose:2.0.0")
}

Quick start

Everything happens inside context.shortcuts { }:

context.shortcuts {
    dynamic("compose_email") {
        shortLabel = "Compose"
        longLabel = "Compose a new email"
        icon = R.drawable.ic_compose
        rank = 1
        intent {
            action = Intent.ACTION_VIEW
            data = "myapp://compose".toUri()
            putExtra("source", "shortcut")
        }
    }
}

dynamic publishes the shortcut, or updates it in place when the id already exists (pushDynamicShortcut under the hood, so the lowest-ranked shortcut is evicted automatically when the launcher limit is reached).

Intents

Each intent { } block supports the full Intent surface: action (defaults to Intent.ACTION_VIEW — the system requires shortcut intents to carry an action), data, type, flags/addFlags, extras of every Bundle type, and an explicit target activity — prefer target<MyActivity>() over implicit resolution so the shortcut cannot be intercepted:

intent {
    target<ComposeActivity>()
    putExtra("draftId", 42L)
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}

Repeat intent { } to build a back stack — the last intent is launched, the earlier ones are what the back button walks through:

dynamic("deep_link") {
    shortLabel = "Order #7"
    intent { target<HomeActivity>() }
    intent { target<OrdersActivity>() }
    intent {
        target<OrderDetailActivity>()
        putExtra("orderId", 7)
    }
}

Icons

icon = R.drawable.ic_compose            // resource id
icon(IconCompat.createWithBitmap(bmp))  // any IconCompat
adaptiveIcon(fullBleedBitmap)           // adaptive bitmap

Pinned shortcuts

context.shortcuts {
    val requested = pinned("call_mom") {
        shortLabel = "Call mom"
        icon = R.drawable.ic_phone
        intent {
            target<CallActivity>()
            putExtra("contact", "mom")
        }
        resultCallback = PendingIntent.getBroadcast(
            context, 0,
            Intent(ACTION_PINNED).setPackage(context.packageName),
            PendingIntent.FLAG_IMMUTABLE,
        )
    }
    if (!requested) { /* launcher doesn't support pinning */ }
}

On API 26+ the system shows its pin-confirmation dialog and fires resultCallback when the user confirms; below API 26 the legacy launcher broadcast is used. Check isPinShortcutSupported up front if you want to hide the UI entirely.

Managing shortcuts

context.shortcuts {
    update("compose_email") { /* new content, never publishes a new id */ }
    remove("old_promo", "older_promo")
    removeAll()
    disable("compose_email", message = "Come back later")
    enable("compose_email")
    reportUsed("compose_email")   // feed the launcher's prediction ranking

    val limit = maxShortcutCountPerActivity
    val throttled = isRateLimitingActive
}

Reading extras in the target activity

There is no callback interface in 2.0 — shortcut launches are plain intent deliveries. Read the extras where they arrive:

class ComposeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        when (intent.getStringExtra("source")) {
            "shortcut" -> shortcuts { reportUsed("compose_email") }
        }
    }

    // If the activity uses launchMode="singleTop", also handle onNewIntent:
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        // re-read extras here
    }
}

Treat shortcut extras like any external input: they survive on the launcher across app updates, so validate values instead of assuming they match the current app version.

Jetpack Compose (shortcut-compose)

A second artifact provides lifecycle-aware Compose bindings on top of shortcut-core (the core module itself stays Compose-free):

DynamicShortcutEffect publishes a dynamic shortcut while it is in composition, re-publishes when its keys change, and (by default) removes it when it leaves composition:

@Composable
fun ComposeScreen(draftCount: Int) {
    DynamicShortcutEffect("compose_email", draftCount) {
        shortLabel = "Compose ($draftCount drafts)"
        icon = R.drawable.ic_compose
        intent { target<ComposeActivity>() }
    }
}

Pass removeOnDispose = false for shortcuts that should outlive the screen.

rememberPinShortcutRequester handles pin requests with result delivery scoped to composition:

val pinRequester = rememberPinShortcutRequester { pinnedId ->
    // user confirmed the pin
}
Button(onClick = {
    pinRequester.request("call_mom") {
        shortLabel = "Call mom"
        intent { target<CallActivity>() }
    }
}) { Text("Pin it") }

Static shortcuts and capabilities (shortcuts.xml)

Static (manifest) shortcuts and App Actions capabilities are declared in XML, not through this library. Add res/xml/shortcuts.xml and reference it from your main activity:

<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity" android:exported="true">
    <meta-data android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
</activity>
<!-- res/xml/shortcuts.xml -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
    <shortcut
        android:shortcutId="compose_static"
        android:enabled="true"
        android:icon="@drawable/ic_compose"
        android:shortcutShortLabel="@string/compose_short_label">
        <intent
            android:action="android.intent.action.VIEW"
            android:targetPackage="com.example.app"
            android:targetClass="com.example.app.ComposeActivity" />
        <capability android:name="actions.intent.CREATE_MESSAGE" />
    </shortcut>
</shortcuts>

Dynamic shortcuts published with this library can participate in Google Assistant App Actions by matching a <capability> declared there; binding dynamic shortcuts to capabilities at runtime additionally requires Google's Shortcuts Integration Library, which this library deliberately does not depend on.

License

   Copyright 2019 Mehdi Khalifeh

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools
AI Daily Digest