Skip to content

Repository files navigation

Delegator ⚠️ Work in Progress

A Kotlin KSP library for multimodule Android projects that automates the wiring of feature module implementations into Dagger multibinding sets.


The Problem

In a multimodule Android app, the core app often needs to delegate work to feature modules without knowing which features exist at compile time. For example, when a push notification arrives, any number of feature modules might want to handle it. Manually maintaining a list of handlers in the app module creates tight coupling and boilerplate.

Delegator solves this with KSP code generation and Dagger multibindings — feature teams register themselves; the app just uses a typed delegator.


Quick Start

Step 1 — Platform engineer defines a namespace

In a shared :app-features module, apply KSP and annotate an interface with @Delegatable:

// :app-features/build.gradle.kts
plugins {
    id("com.google.devtools.ksp")
}
dependencies {
    api("io.github.saadfarooq:delegator-annotations:x.y.z")
    ksp("io.github.saadfarooq:delegator-processor:x.y.z")
}
// AppFeatures.kt
@Delegatable
interface AppFeatures {

    // Extending DelegatableCleanup enforces cleanup on all implementations
    interface PushNotificationHandler : DelegatableCleanup {
        fun canHandle(message: PushMessage): Boolean
        fun handle(message: PushMessage)
    }

    interface PillarCell {
        fun render(data: PillarCellData): String
    }
}

KSP generates:

  • @AppFeaturesDelegatable — typed registration annotation for feature teams
  • AppFeaturesPushNotificationHandlerDelegator — injectable class with getHandlers() and runCleanup()
  • AppFeaturesPillarCellDelegator — same for PillarCell
  • Empty-set Dagger base modules so the graph is valid with zero implementations

Step 2 — Feature team registers their implementations

In their feature module, they apply KSP and use the generated @AppFeaturesDelegatable annotation once. IDE autocompletes the parameter names; the compiler rejects wrong types. Parameters not provided default to "not participating":

// :feature-inbox/build.gradle.kts
plugins {
    id("com.google.devtools.ksp")
}
dependencies {
    implementation(project(":app-features"))
    ksp("io.github.saadfarooq:delegator-processor:x.y.z")
}
// InboxFeatures.kt
@AppFeaturesDelegatable(
    pushNotificationHandler = InboxPushNotificationHandler::class,
    pillarCell = InboxPillarCell::class,
    guard = InboxGuard::class,
)
interface InboxFeatures

The guard references the team's own feature flag system — the library is agnostic:

object InboxGuard : DelegatableGuard {
    override val isEnabled get() = Statsig.checkGate("inbox_enabled")
}

Implementations are plain classes — no library annotations required:

class InboxPushNotificationHandler @Inject constructor(
    private val notificationManager: NotificationManager
) : AppFeatures.PushNotificationHandler {

    override fun canHandle(message: PushMessage) = message.type == "inbox"
    override fun handle(message: PushMessage) { TODO() }

    // Required — PushNotificationHandler extends DelegatableCleanup
    override fun cleanup() {
        notificationManager.deleteNotificationChannel("INBOX_NOTIFICATIONS")
    }
}

class InboxPillarCell @Inject constructor() : AppFeatures.PillarCell {
    override fun render(data: PillarCellData): String = TODO()
}

KSP generates InboxFeaturesDelegatableModule — a Dagger @Module with @Provides @IntoSet for each implementation, wrapped in Guarded<T> alongside the feature's guard.


Step 3 — App engineer uses the delegator

The generated delegator is injectable. getHandlers() returns only implementations whose guard is enabled; runCleanup() calls cleanup() on implementations whose guard is disabled:

class AppPushService @Inject constructor(
    private val delegator: AppFeaturesPushNotificationHandlerDelegator
) {
    fun onAppStart() = delegator.runCleanup()

    fun onPush(message: PushMessage) {
        delegator.getHandlers()
            .firstOrNull { it.canHandle(message) }
            ?.handle(message)
    }
}

How It Works

:app-features module
  @Delegatable interface AppFeatures          ← you write this
      └── KSP generates ──────────────────→  @AppFeaturesDelegatable annotation
                                              AppFeaturesPushNotificationHandlerDelegator
                                              AppFeaturesPushNotificationHandlerBaseModule

:feature-inbox module
  @AppFeaturesDelegatable(...)                ← you write this
  interface InboxFeatures
      └── KSP generates ──────────────────→  InboxFeaturesDelegatableModule
                                                @Provides @IntoSet Guarded<PushNotificationHandler>
                                                @Provides @IntoSet Guarded<PillarCell>

Dagger assembles the Set<Guarded<T>> across all feature modules. The delegator filters by guard at runtime.


Enforcing Cleanup

Interface authors can require cleanup by extending DelegatableCleanup. Feature teams get a compile error if cleanup() is missing:

interface PushNotificationHandler : DelegatableCleanup { ... } // cleanup required
interface PillarCell { ... }                                    // cleanup optional

Modules

Module Description
delegator-annotations @Delegatable, @DelegatableRegistration, DelegatableGuard, DelegatableCleanup, Guarded<T>
delegator-processor KSP processor — generates registration annotations, delegators, and Dagger modules
sample-app-features Example: platform team defines AppFeatures namespace
sample-feature-inbox Example: Inbox team registers via @AppFeaturesDelegatable

Status

  • @Delegatable → generates @{Name}Delegatable annotation with bounded KClass params
  • @Delegatable → generates {Name}{Interface}Delegator with guard-filtered getHandlers() and runCleanup()
  • @Delegatable → generates empty-set base Dagger module
  • @{Name}Delegatable → generates {Feature}DelegatableModule with @Provides @IntoSet Guarded<T>
  • Hilt @InstallIn support
  • Compile-time validation (type mismatch, missing interface implementation)
  • Gradle convenience plugin
  • Publishing to Maven Central

About

Escapsulate functionality in feature modules

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages