DevView
Why does this exist?
If I’m being perfectly honest, I built this because it was a fun challenge a colleague set me and the conversation went something like this.
Colleague: Do you think we could build something to toggle feature flags? Like without publishing a new app.
Me: Yes? Why though?
Colleague: Do you think we could do it for KMP? Like, something that would work on Android and iOS?
Me: Again, yes, but why?
Colleague: Could it also track the analytics history?
Me: …
Colleague: And mock individual APIs?
Me: …
You can see where this conversation was going. The following 30+ minutes consisted of various exchanges to pinpoint specifics of what they had in mind, issues we’d face and a never-ending list of “things that could be interesting to add”.
You know, the things that motivate you as an engineer and developer, but that never seem to warrant the necessary time and resource allocation needed to actually be done? Yes, those ones. A whole list of them!
I said yes. To all of them.
Side note for other developers: Don’t do this, please learn to say no…
I did this because I had an opportunity that doesn’t show its face very often: Dedicated time.
As a Lead Developer at Worldline, I’m often faced with situations where
- APIs are down but testers are expected to be checking our work
- Clients raise issues about incorrect analytics being logged
- Features should be gated by flags but handling the flags requires some server and/or application setup
Every time, the solution is a mix of: pushing a new release with mocks, checking the analytics logs locally, asking the backend team to add the flag to the API, realise the flag added has a typo, handle typo in-app and ask backend team to fix spelling, etc.
It’s long-winded, it’s cumbersome, it’s not interesting and worst of all, it wastes valuable time you could be spending on that really cool translation issue ticket you’ve been putting off on purpose for… reasons.
This dedicated time allowed me to sit down with the other developers in my team and imagine what the tool that would get rid of a variety of debugging and testing pain points would look like. The result?
DevView .
What is DevView?
DevView is an in-app overlay that gives developers a set of tools to configure and inspect the running app — no rebuild required. Trigger it however you like — a button, a shake, a gesture — and it slides in over your app content.
As of today, the tools available are:
- Feature Flags — Toggle and override without rebuilding
- Analytics — Inspect analytics events and history (for a session)
- Network mocking — Swap out API responses per endpoint
Each tool is published as a standalone artifact so you can pull only what you need.
Don’t see anything that fits your need? The Module interface allows you to implement your own DevView modules and plug them in directly, no questions asked.
Custom module support is actively evolving — the next article in this series covers building one from scratch.

Let’s tour the modules!
Feature Flags
The featureflip module supports two types of flags:
- Local flags: Simple booleans managed in-app. Perfect for development-time toggles or testing configurations.
- Remote flags: Tri-state flag that toggles between
REMOTE,LOCAL_ONandLOCAL_OFF. This allows testing both states of a remotely-controlled feature regardless of what the backend says.
All flag states are persisted with DataStore
, meaning any changes you make survive app restarts.

Analytics
The analytics module provides a real-time log viewer for your analytics events. Simply call the AnalyticsLogger.log() function with the appropriate event and you’ll find it in the specific screen within the DevView.
Events are categorised into 11 built-in types which should cover most common use cases.
You can filter by category, time or free text and the log counter highlights the amount for each type so you can spot missing/duplicate events easily.

Network mocking
Arguably the most interesting module so far, the networkmock module allows toggling responses on a per-API basis. No proxy setup, no **BuildConfig** , just pick and choose!
Define your mocks in the mocks.json config file, which supports multiple API groups, multiple environments and path parameters like /users/{userId} .
The networkmock-ktor comes with the NetworkMockPlugin that intercepts requests client-side via Ktor
’s HttpSend pipeline and returns the specified mock response. If mocking is disabled or no mocks exist for that endpoint, it “falls through” to the real network call.
The setup is minimal — a single line in your Ktor client config. The mock config file format does need to be correct though, so make sure to follow the documentation closely.

Integration time!
I despise complex libraries, which is why I based a lot of my work on the haze library, a library so naively simple to add to a project you forget the complexity of what it actually does.
The aim here was to do the same (or close to it) so let’s run through the setup.
Step 1: Add the dependencies
The library is built to be modular — Add only the dependencies you need and you’re good to go!
// build.gradle.kts (shared)
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("com.worldline.devview:devview:<version>") // core — always required
implementation("com.worldline.devview:devview-featureflip:<version>") // feature flags
implementation("com.worldline.devview:devview-analytics:<version>") // analytics inspector
implementation("com.worldline.devview:devview-networkmock:<version>") // network mock UI
}
}
}
}
// build.gradle.kts (data/network)
kotlin {
sourceSets {
commonMain {
dependencies {
// Ktor plugin — lightweight, no UI dependency
implementation("com.worldline.devview:devview-networkmock-ktor:<version>")
}
}
}
}
Step 2: Register your modules
The entry point is the rememberModules { } DSL. You declare which modules you want, and the framework handles the rest — DataStore initialization, module setup, lifecycle:
val modules = rememberModules {
module(module = FeatureFlip)
module(module = Analytics())
module(
module = NetworkMock(
resourceLoader = { path -> Res.readBytes(path = path) }
)
)
}
Notice there’s no annotation processing, no code generation, no reflection. Modules are just objects. If you want to conditionally include one — say, only in debug builds — it’s a plain
ifstatement inside the lambda.
Step 3: Wire up the modules’ behaviour
Feature flags and analytics use CompositionLocals to propagate their state through the composition tree. Here’s how you set them up:
val darkTheme = isSystemInDarkTheme()
val featureHandler = rememberFeatureHandler(
features = listOf(
Feature.LocalFeature(
name = "Dark Mode",
description = "Enable or disable dark mode",
isEnabled = darkTheme
)
)
)
CompositionLocalProvider(
LocalFeatureHandler provides featureHandler,
LocalAnalytics provides AnalyticsLogger.logs
) {
// Your app + DevView go here
}
Step 4: Show the DevView
Since Compose renders elements in order, add your App first, then the DevView — this ensures the overlay renders on top.
var devViewOpen by remember { mutableStateOf(false) }
App(openDevView = { devViewOpen = it })
DevView(
devViewIsOpen = devViewOpen,
closeDevView = { devViewOpen = false },
modules = modules
)
Step 5 (optional): Ktor plugin
If you’re using the network mocking module with Ktor, you simply need to install the plugin into your existing HttpClient :
install(plugin = NetworkMockPlugin)
In addition, you also need to define the mocks.json file (and others) to tell the plugin what it can do for what APIs.
That’s it. No manual repository wiring. NetworkMockPlugin resolves its dependencies from the NetworkMockInitializer that was populated when rememberModules ran. Just make sure rememberModules { } has been composed before your client makes its first request.
Important note: The mocking behaviour today uses an
@InternalAPIfrom Ktor. I’m working on finding a way to remove this and guarantee the behaviour will continue to work in future versions
Total integration: roughly 30 lines of Kotlin. The rest is your app.
What if I need something else?
Tough luck.
I’m kidding. I also enjoy being able to extend libraries with some app-specific elements for my needs. Libraries like chucker offer great features but prevent integrators from extending the library to better suit their needs, and this has always been a pain point for me for libraries in general.
The modules presented so far are all built from the same Module interface. Don’t see what you need? Implement Module. Add to the registry. Done.
Here’s the interface:
interface Module {
val section: Section // where it appears on the home screen
val moduleName: String // display name
val icon: ImageVector // home screen icon
val destinations: PersistentMap<KClass<out NavKey>, DestinationMetadata>
val entryDestination: NavKey // screen to open when the user taps the card
val registerSerializers: PolymorphicModuleBuilder<NavKey>.() -> Unit
fun EntryProviderScope<NavKey>.registerContent(
onNavigateBack: () -> Unit,
onNavigate: (NavKey) -> Unit,
bottomPadding: Dp
)
}
Destinations are @Serializable Navigation3
NavKey types. Content is registered as Composables per-destination.
The whole thing — two screens, type-safe navigation, proper serialization for process death survival — comes in at around 40 lines.
In the next article, we’ll build a custom module from scratch and dig into how the architecture actually works: the Module interface contract, how Navigation3 ties it all together, and the plugin system that makes it extensible.
That’s all folks!
for now…
DevView gives your KMP application an in-app developer overlay. Pick the modules you need, add about 30 lines of Kotlin, and you’re done.
You can find everything you need here:
- GitHub: https://github.com/worldline/devview
- Documentation: https://worldline.github.io/devview/
- Maven Central: com.worldline.devview
Massive shoutout to the people that inspired me to publish something open-source and that guided me to making this a reality:
