Library / SDK
badoo/MVICore avatar
badoo/MVICore

MVICore: a Kotlin MVI framework with time travel debugging

MVI framework with events, time-travel, and more

1,302 stars87 forksKotlinNOASSERTION

At a glance

What is it?
MVICore is Badoo's Kotlin MVI framework, published as a set of separate modules with a reducer at the centre, an event mechanism for things that should not live in state, and a time travel debugger that records and replays every reactive component. It is distributed through Jitpack rather than Maven Central, which is the first practical thing to check before adopting it.
Who is it for?
MVICore suits an Android team that has outgrown a single state holder, wants events kept out of the state, and values being able to replay a bug from a recording. It does not suit a project that needs a stable, versioned artefact from a standard repository, or a team that has no appetite for an architecture with several moving parts.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

MVICore is Badoo's take on MVI for Android

MVICore is a Kotlin MVI framework that came out of Badoo, whose engineering work appears under the Bumble Tech name on Medium, and the repository is where the library itself lives.

The framing in the project documentation is worth reading closely, because it explains why the library is more than a reducer. MVICore is presented as a set of capabilities rather than a single pattern: scaling with complexity, so that you can operate with a single Reducer if that is all you need, with the option of using the additional components when the case is more complex; event handling, described as a solution for handling events that you do not want to store in the state; reactive component binding with automatic lifecycle handling; custom middlewares for every component; a logger middleware out of the box; and a time travel debugger.

That last combination is the differentiator. Most MVI libraries stop at the state machine. This one treats the whole reactive graph as debuggable, and the supporting material is a set of write-ups on MVI beyond state reducers, on building a system of reactive components with Kotlin, and on unidirectional data flow and the Zen of black box components, plus a conference talk on time travel debugging.

The audience is an Android team that has already tried a plain state holder and found it limiting, and that wants a structured way to compose several reactive pieces without losing track of the data flow.

One reducer or several components, chosen per problem

The first design decision the framework makes is about how much machinery a screen needs. The documentation describes operating with a single Reducer when that is enough, and adding further components when the problem is more complex.

That is an unusual position for a framework to take, and a sensible one. A large part of the criticism aimed at MVI is that a screen with one state class and one reducer function gets wrapped in a ceremony that buys nothing. Here, the reducer is the base unit rather than the whole system, and the extra parts are available when a feature actually needs them.

The consequence for a developer is that the choice of architecture is made per feature rather than per application. One screen might be a reducer and a set of intents, while another needs the event mechanism and several bound components, and both sit under the same middleware pipeline.

The related articles in the repository make the same argument from the other side, describing MVI beyond state reducers and a system of reactive components built with Kotlin. Reading them is a reasonable way to decide whether the extra components are something your team would use or something you would only pay for.

Events are the part that stays out of the state

The event handling feature is described in one line, and it is the line that tells you what the framework is for. It is a solution to handling events that you do not want to store in the state.

The problem it solves is real and common. In a strict unidirectional setup, everything the user can do becomes an intent, and everything the intent produces becomes state, so a one-off event such as a toast, a navigation action or a transient error ends up either as a field in the state that has to be consumed and cleared, or as a side effect that breaks the flow guarantee. Both workarounds are unsatisfying.

A separate event channel gives those one-offs a home without polluting the state. The important consequence is architectural rather than syntactic. If the state contains only what the UI needs to render, then it becomes a good candidate for diffing, for persistence, for replay, and for the time travel debugger. The state stops being a dumping ground and becomes a serialisable description of the screen.

That is also why the framework can offer a time travel debugger for all of the reactive components and not just for the state machine. The state is one observable in a graph of observables, and the debugger works on the graph.

The binder connects reactive endpoints with lifecycle handling

The reactive component binding feature is described as a super simple API to bind your reactive endpoints to each other with automatic lifecycle handling.

Two things are packed into that sentence. The first is the binding itself, which removes the boilerplate of subscribing one stream to another and writing the teardown code by hand. The second is the lifecycle handling, which is where most reactive code goes wrong. A subscription that outlives its screen, or one that is registered before the view exists, is the classic source of leaks and of crashes on rotation, and the automatic part is what stops the library from being another source of those bugs.

The binder is a separate module rather than part of the core, and the README states that it is for versions higher than 1.2.4. That detail matters for dependency resolution, because a project on an older MVICore version cannot use the binder module at all and has to bind its own endpoints.

The repository also carries a diff module, described as a ModelWatcher for efficient view updates, which is the companion to the binder. If the model changes wholesale, a watcher that computes the minimal set of view updates is what keeps a list screen from rebinding everything on each emission.

Time travel debugging covers every reactive component

The time travel debugger is the feature that has no equivalent in a plain state holder, and the project documentation is specific about its scope. It works for all of your reactive components, not just your state machine, with UI controls for recording and playback.

The mechanism follows from the architecture. If each component exposes its state as an observable, and the framework can subscribe to and record those, then a session becomes a sequence of recorded emissions. Replaying them drives the same graph, and the UI is reconstructed from what was recorded rather than from what the user did. A crash on a specific interaction sequence can therefore be reproduced without a device in hand, and the person reporting the bug can hand over a recording instead of a description.

The controls ship in a separate DebugDrawer module, and the README is explicit that you have to add the dependencies to DebugDrawer and configure it yourself before you can use it. That is a real integration cost, since a debug drawer is an in-app surface that has to be wired into the app's navigation and given a place to live.

There is also a logger middleware, which sounds redundant next to a time travel debugger and is not. The logger gives you a live trace of what the components are doing, while the debugger lets you step back into what already happened.

The modules are separate artifacts on Jitpack

Installation is the part of this project that deserves a careful look before you commit, because the library is distributed through Jitpack and split into several modules.

The repository has to be added to your root build.gradle first:

groovy
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

Then the modules are added as dependencies, each with its own coordinate:

groovy
implementation 'com.github.badoo.mvicore:mvicore:{latest-version}'
implementation 'com.github.badoo.mvicore:binder:{latest-version}'
implementation 'com.github.badoo.mvicore:mvicore-android:{latest-version}'
implementation 'com.github.badoo.mvicore:mvicore-diff:{latest-version}'
implementation 'com.github.badoo.mvicore:mvicore-debugdrawer:{latest-version}'

The framework core is `mvicore`, the binder is `binder`, the Android helper classes are `mvicore-android`, the ModelWatcher for efficient view updates is `mvicore-diff`, and the debugger controls are `mvicore-debugdrawer`. You take only what you use, which is the right shape for a library that spans the range from a single reducer to a full reactive system.

The repository layout confirms the split, with top level directories for `binder/`, `mvicore/`, `mvicore-android/`, `mvicore-debugdrawer/`, `mvicore-diff/`, a `mvicore-demo/`, a `mvicore-plugin/`, a `plugins/` folder, and a `documentation/` folder, and an mkdocs configuration. The build is Gradle with a `settings.gradle.kts`, a `gradlew` wrapper, and a detekt configuration for static analysis.

Using Jitpack rather than Maven Central has a practical consequence. The coordinates are the GitHub path and the module name, and the version placeholder in the README has to be filled in with a real tag. There is no BOM and no stable channel to depend on, so a team should pin an explicit version rather than chase a moving target.

Where a plain state holder is the better answer

The alternative worth considering is not another MVI library but the simplest possible approach: a state holder per screen, with the state as a single observable, and nothing else.

The difference in approach is straightforward. A plain state holder gives you one class of state, one function to change it, and a subscription to observe it. It has no middleware chain, no event channel, no bound components, no recorded sessions, and no tooling beyond whatever your IDE gives you. MVICore gives you all of that, and it gives you the option to use it per feature, but every additional component is a concept your team has to learn, name, and debug.

The trade-off is sharpest on a small screen. A login form with three fields does not need a time travel debugger, and a team that adopts the framework wholesale will pay the learning cost on every screen. The framework is at its best when a feature has genuinely several reactive parts that need to talk to each other, or when a bug that only appears in a specific interaction sequence is costing more than the tooling does.

There is a second limitation to plan for, and it is the one that decides whether MVICore fits at all. The library is published through Jitpack, and its release history is uneven. Version 1.4.0 shipped on 2023-02-09, 1.3.1 on 2022-08-10, and 2.0.0 on 2025-04-02. The repository is not archived and the last push was on 2026-10-16, so work continues, but the most recent tagged release is well over a year old. A framework at version 2.0.0 that is distributed from a GitHub-based build service is a dependency with more moving parts than a library from a stable registry, and that is a fact to weigh before the architecture is committed to.

Editorial conclusion

MVICore suits an Android team that has outgrown a single state holder, wants events kept out of the state, and values being able to replay a bug from a recording. It does not suit a project that needs a stable, versioned artefact from a standard repository, or a team that has no appetite for an architecture with several moving parts. Check first which MVICore version your code targets, since the binder module is only shipped for versions above 1.2.4 and the 2.0.0 release postdates a long gap from the 1.4.0 line.

Frequently asked questions

How do I add MVICore to an Android project?

Add the Jitpack repository to your root build.gradle with `maven { url 'https://jitpack.io' }`, then add the modules you need, for example `implementation 'com.github.badoo.mvicore:mvicore:{latest-version}'` for the framework, with separate coordinates for the binder, the Android helpers, the diff module and the debug drawer.

What does the event handling in MVICore do?

It is described as a solution to handling events that you do not want to store in the state. It gives one-off actions such as navigation or transient errors a separate channel, so the state stays a clean description of what the UI renders.

What is the time travel debugger in MVICore?

It records and replays sessions for all of your reactive components, not only the state machine, with UI controls for recording and playback. The controls live in a separate mvicore-debugdrawer module, and you have to add the DebugDrawer dependencies and configure it yourself before using it.

Which MVICore version should a new project use?

The most recent tagged release is 2.0.0, published on 2025-04-02, and the binder module is only shipped for versions higher than 1.2.4. The library is distributed through Jitpack rather than Maven Central, so pin an explicit version instead of relying on a moving latest tag.

Official sources

  1. badoo/MVICore on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/badoo-mvicore.svg)](https://hysenlabs.com/projects/badoo-mvicore)

Was this useful?