Creating a Custom Plugin¶
Build your own first-class Sidekick plugin by implementing the SidekickPlugin interface from :core:plugin-api. Use this when you want a self-contained module with its own DI scope, data layer, and UI — for example, a Room-backed event log, a feature-flag editor with a remote backend, or a runtime DSL inspector. For simpler one-off screens, prefer Custom Screens.
Platforms¶
Your plugin inherits the targets you publish for. The SidekickPlugin interface itself is commonMain-only.
Features¶
- One interface —
id,title,icon,@Composable Content(). - No required dependencies — just
:core:plugin-api. Bring your own data layer, DI, and state management. - Lifecycle-aware — implement
SidekickLifecycleAwareif you needonPanelOpened/onPanelClosedhooks. - Back-navigation built in — call
LocalSidekickBackNavigator.currentto return to the plugin grid.
Modules¶
| Module | Purpose |
|---|---|
:core:plugin-api |
SidekickPlugin, SidekickAppInfo, LocalSidekickBackNavigator, SidekickLifecycleAware. |
| Your own module | Implement SidekickPlugin, ship the data layer + UI. |
Setup¶
1. Add dependency¶
Your plugin module should compile against plugin-api:
// your-plugin/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(platform("dev.parez.sidekick:bom:2026.05.17"))
implementation("dev.parez.sidekick:plugin-api") // version from BOM
}
}
}
2. Implement SidekickPlugin¶
class LogsPlugin : SidekickPlugin {
override val id: String = "com.myapp.logs"
override val title: String = "Logs"
override val icon: ImageVector = Icons.Default.Article
@Composable
override fun Content() {
LazyColumn(Modifier.fillMaxSize()) {
items(LogBuffer.entries) { entry ->
ListItem(
headlineContent = { Text(entry.message) },
supportingContent = {
Text(entry.tag, style = MaterialTheme.typography.labelSmall)
},
)
}
}
}
}
3. Wire into Sidekick¶
Pass it alongside any other plugins:
Sidekick(
plugins = listOf(networkPlugin, prefsPlugin, LogsPlugin()),
actions = {
IconButton(onClick = { sidekickVisible = false }) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
},
)
Configuration¶
| Member | Constraint |
|---|---|
id: String |
Must be unique across all plugins passed to Sidekick. Prefer reverse-domain (e.g. "com.myapp.logs"). |
title: String |
Shown in the plugin-grid card and the screen header. |
icon: ImageVector |
Shown in the plugin-grid card. Material icons recommended for consistency. |
@Composable Content() |
Renders inside the active MaterialTheme. Fills the plugin panel area — use Modifier.fillMaxSize() on the root. |
UI¶
Content()runs under the host's activeMaterialTheme(or Sidekick's, depending onuseSidekickTheme).MaterialTheme.colorSchemeis available.- For reactive state, use
StateFlowcollected withcollectAsState()orcollectAsStateWithLifecycle(). - For adaptive layouts, use
BoxWithConstraintsor the M3 AdaptiveListDetailPaneScaffoldwith breakpoints at 600 dp (medium) and 840 dp (expanded). Both built-in monitor plugins use this pattern. - To navigate back to the plugin grid programmatically (e.g. a "Close" button inside your screen), call
LocalSidekickBackNavigator.current().
Advanced¶
Lifecycle hooks¶
Implement SidekickLifecycleAware if your plugin needs to know when the panel opens or closes (e.g. start / stop polling):
class LogsPlugin : SidekickPlugin, SidekickLifecycleAware {
override fun onPanelOpened() { startTailing() }
override fun onPanelClosed() { stopTailing() }
/* … */
}
Scaffolding with Claude Code¶
Use the /create-plugin skill to scaffold a new plugin module from scratch — it creates the build.gradle.kts, base implementation class, and registers the module in settings.gradle.kts.
See also¶
- Custom Screens — simpler wrapper when you just want a Composable in the panel.
- Network Monitor and Log Monitor for reference implementations.