post-thumb

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.

DevView Home Screen

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_ON and LOCAL_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.

DevView Feature Flip screen

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.

DevView Analytics screen

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.

DevView Network Mock screen

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 if statement 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 @InternalAPI from 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:

Massive shoutout to the people that inspired me to publish something open-source and that guided me to making this a reality: