Overview

Analytics implementations suffer from a common problem: event names and parameter keys are stringly-typed, scattered across the codebase, and easy to misspell. The schema lives in someone's head or a Google Sheet — not enforced by the compiler.

typed-events solves this by making your schema the source of truth. You write a YAML file that describes every event — its name, a description, and typed parameters. The plugin generates top-level Kotlin functions, one per event, that forward to a handler you register once at startup. No inheritance, no boilerplate.

Typed parameters

All parameters are real Kotlin types — compiler errors instead of runtime crashes.

Single registration point

Call registerTypedEventHandler once at startup to connect any backend.

KDoc generated

Every function and parameter gets a doc comment from the YAML info fields.

Schema as source of truth

The YAML lives in version control alongside the code it drives.

Configuration cache compatible

The generation task is fully incremental and compatible with Gradle's configuration cache.

Installation

Add Maven Central to your plugin repositories in settings.gradle.kts:

settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

Then apply the plugin and point it at your schema file:

build.gradle.kts
plugins {
    id("com.rohittp.plugables.typed-events") version "1.0.0"
}

typedEvents {
    specFile.set(file("src/main/analytics/events.yaml"))
}

specFile is the only required property. Everything else has a sensible default.

Requires AGP 7.2+

The plugin wires generated sources via the Android Variant API (AndroidComponentsExtension). Android Gradle Plugin 7.2 or higher is required. The plugin is tested against AGP 9.1.0.

Recommended location

Place your schema at src/main/analytics/events.yaml. Keeping it under src/main means it lives alongside the code it describes and participates in standard VCS workflows.

YAML Schema

The schema is a YAML list where each item defines one analytics event. Two forms are supported: a verbose full form for events with parameters, and a one-line shorthand for parameter-less events.

Full form

Use the full form when an event carries parameters.

events.yaml
# Full form — use when the event has parameters
- purchase_completed:
    info: User completed a purchase flow
    function: logPurchaseCompleted  # optional — auto-derived if omitted
    params:
      item_id:
        type: String
        info: Unique identifier of the purchased item
      price:
        type: Double
        info: Final price paid after discounts, in the user's currency
      currency:
        type: String
        info: ISO 4217 currency code (e.g. USD, EUR)

Full form fields

Field Required Description
info Yes Human-readable description of the event. Becomes the KDoc comment on the generated method.
function No Override the generated method name. If omitted, the name is derived from event_name by camel-casing with a log prefix — e.g., purchase_completedlogPurchaseCompleted.
params No Map of parameter definitions. Omit entirely for parameter-less events.
params.*.type Yes (per param) Any valid Kotlin type string, e.g. String, Double, Boolean, String?.
params.*.info Yes (per param) Description of the parameter. Becomes a @param KDoc entry.

Shorthand form

For parameter-less events, collapse the definition to a single line. The value is the event description (the info field).

events.yaml
# Shorthand — event name: description
- screen_viewed:    User navigated to a new screen
- app_opened:      App launched or foregrounded
- logout_tapped:   User tapped the logout button

Validation rules

The task validates the schema before generating. Build fails with a clear message if any rule is violated:

  • info must be present and non-blank on every event and every parameter.
  • Event names must be unique within the file.
  • Function names (derived or explicit) must be unique within the file.
  • Each params entry must have both type and info.
  • Derived function names must be valid Kotlin identifiers.

Parameter name derivation

YAML parameter keys are converted to camelCase Kotlin identifiers by splitting on non-alphanumeric characters, lowercasing the first segment, and title-casing the rest. A leading digit causes a _ prefix.

YAML keyKotlin parameter name
item_iditemId
screen_namescreenName
priceprice
2fa_enabled_2faEnabled

Configuration

The typedEvents DSL block exposes two properties:

Property Type Required Default Description
specFile RegularFileProperty Yes Path to the YAML schema file. Build fails if unset or file not found.
outputDir DirectoryProperty No build/generated/source/typedEvents/main Where AnalyticsBase.kt is written. Automatically wired into the AGP main source set.

All generated code is placed in the package com.rohittp.plugables.analytics — this is fixed and not configurable.

Generated API

Given the following schema combining both full and shorthand forms:

events.yaml
- purchase_completed:
    info: User completed a purchase
    params:
      item_id:
        type: String
        info: The purchased item ID
      price:
        type: Double
        info: Final price paid
- screen_viewed: User viewed a screen

The plugin generates two files:

TypedEventHandler.kt

The registration surface. Call registerTypedEventHandler once at startup to wire in your backend.

generated kotlin — TypedEventHandler.kt
package com.rohittp.plugables.analytics

// GENERATED FILE. Do not edit.

internal var typedEventHandler: ((eventName: String, params: Map<String, Any?>) -> Unit)? = null

/**
 * Registers the analytics event handler.
 *
 * Call once at app startup, e.g. inside Application.onCreate:
 *   registerTypedEventHandler(::logEvent)
 */
fun registerTypedEventHandler(handler: (eventName: String, params: Map<String, Any?>) -> Unit) {
    typedEventHandler = handler
}

AnalyticsEvents.kt

One top-level function per event. Each function validates that a handler has been registered (via assert) then forwards to it.

generated kotlin — AnalyticsEvents.kt
package com.rohittp.plugables.analytics

// GENERATED FILE. Do not edit.
// Source: events.yaml

/**
 * User completed a purchase
 * @param itemId The purchased item ID
 * @param price Final price paid
 */
fun logPurchaseCompleted(itemId: String, price: Double) {
    assert(typedEventHandler != null) { "registerTypedEventHandler() must be called before logging events" }
    typedEventHandler?.invoke("purchase_completed", mapOf("item_id" to itemId, "price" to price))
}

/** User viewed a screen */
fun logScreenViewed() {
    assert(typedEventHandler != null) { "registerTypedEventHandler() must be called before logging events" }
    typedEventHandler?.invoke("screen_viewed", emptyMap())
}

Note on assert

assert is only active when the JVM is run with -ea. In production Android builds the check is a no-op, so forgetting to register will silently drop events rather than crash. Register the handler at the start of Application.onCreate() to make it impossible to forget.

Usage

Register a handler once at startup, then call the generated top-level functions anywhere in the app.

Step 1 — Register the handler

Call registerTypedEventHandler in Application.onCreate() before any event functions are invoked.

kotlin
import com.rohittp.plugables.analytics.registerTypedEventHandler

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        registerTypedEventHandler { eventName, params ->
            // forward to your backend here
        }
    }
}

Step 2 — Call the typed functions

The generated functions are top-level — import and call them directly. Full IDE autocomplete and compiler-checked types.

kotlin
import com.rohittp.plugables.analytics.logPurchaseCompleted
import com.rohittp.plugables.analytics.logScreenViewed

logPurchaseCompleted(
    itemId = "sku_abc123",
    price  = 9.99
)

logScreenViewed()

Integrations

The handler receives a plain String event name and a Map<String, Any?>, making it trivial to bridge to any SDK. Pass the lambda directly to registerTypedEventHandler.

Firebase Analytics

kotlin
import com.rohittp.plugables.analytics.registerTypedEventHandler

registerTypedEventHandler { eventName, params ->
    val bundle = Bundle().apply {
        params.forEach { (key, value) ->
            when (value) {
                is String  -> putString(key, value)
                is Double  -> putDouble(key, value)
                is Int     -> putInt(key, value)
                is Boolean -> putBoolean(key, value)
                null       -> {}
                else       -> putString(key, value.toString())
            }
        }
    }
    firebase.logEvent(eventName, bundle)
}

Mixpanel

kotlin
registerTypedEventHandler { eventName, params ->
    val props = JSONObject(params.filterValues { it != null })
    mixpanel.track(eventName, props)
}

No-op (testing / development)

Register an empty lambda for tests or debug builds where you don't want real events sent.

kotlin
// No-op — swallow all events (tests, debug builds)
registerTypedEventHandler { _, _ -> }

// Logging — print events in debug builds
registerTypedEventHandler { eventName, params ->
    Log.d("Analytics", "${eventName} ${params}")
}