Feature Summary

This page is a comparison of several MVI libraries, to help you understand how each library is similar or different from the others. I sincerely believe Ballast is the best option for MVI state management in Kotlin, but that doesn't mean the other libraries aren't good options too. Some of them might have a API that just clicks with you better, and that's perfectly fine. This comparison can help you figure out if Ballast is the right option for you, and if not, help you determine your suitable alternative.

The obvious disclaimer is that this list is put together by the person behind Ballast, so I'm obviously a bit biased toward my own library. But I really do want this to be as objective of a comparison as possible, so if you see any errors or anything seems misleading, please let me know or submit a pull request to correct it!

And to further combat bias, I'd recommend also checking out this article for a more in-depth comparison of these Android/Kotlin MVI libraries, which doesn't include Ballast. This article is from one of the developers of Orbit MVI.

The following libraries are compared in this article:

Legend

  •  Fully Officially supported  
  •  Fully supported by 3rd-party  
  •  Partially supported  
  •  Not supported  

General

General Philosophy

Info

This refers to the general development philosophy behind the development of the library, such as whether it's aiming to be lightweight or fully featured, as well as any other significant notes about how to approach the library.

  • Ballast: Opinionated Application State Management framework for all KMP targets
  • Redux: Lightweight JS UI State Management library, with many official and unofficial extensions
  • Orbit: Fully-featured, low-profile UI MVI framework for Android
  • MVIKotlin: Redux implementation in Kotlin for Android
  • Uniflow-KT:

MVI Style

Info

MVI Style refers to the general API of the library: Redux-style sends discrete objects to the library and uses some kind of transformer class to split out the objects into discrete streams for each input type. Additionally, a true Redux style only transforms state, with mapper functions receiving the current state and returning the updated state, typically called a reducer ((State, Input)->State).

The MVVM+ style discards the discrete input classes, and instead offers helper functions within the ViewModel to translate function calls on the ViewModel into lambdas that are processed in the expected MVI manner. MVVM+ typically offers a richer API, more functionality, and reduced boilerplate, but makes it less obvious what's actually going on within the library.

  • Ballast: Redux-style discrete Inputs with MVVM+ style DSL
  • Redux: Redux
  • Orbit: MVVM+
  • MVIKotlin: Redux
  • Uniflow-KT: MVVM+

Kotlin Multiplatform Support

Info

Whether this library is available for Kotlin Multiplatform, or is limited to a single platform.

  • Ballast:
  • Redux:
  • Orbit:
  • MVIKotlin:
  • Uniflow-KT:

Opinionated structure

Info

MVI is a lert lightweight design pattern overall, not really mandaing much in terms of classes, naming conventions, etc. But being so lightweight can make it difficult to get started if you're not comfortable with the MVI model, so it can be helpful to have a library be opinionated about how it should be used, so you can more easily copy-and-paste code snippets to make it easier to try out on your own.

  • Ballast:  
  • Redux:  createSlice() in Redux Toolkit defines an opinionated structure
  • Orbit:  Intentionally unopinionated. "MVI without the baggage. It's so simple we think of it as MVVM+"
  • MVIKotlin:  
  • Uniflow-KT:  Intentionally unopinionated

Reduced boilerplate

Info

With the MVI model comes a fair amount of boilerplate. Between creating the ViewModel/Store, defining the contract for your State and Intents, and wiring everything up in your application code, it can be a bit overwheling. This section shows how each library attempts to wrangle that boilerplate and make it more approachable for new users, and less tedious for long-time users.

  • Ballast:  Templates/scaffolds available in Official IntelliJ Plugin
  • Redux:  createSlice() in Redux Toolkit reduces boilerplate
  • Orbit:  The whole framework was created to reduce boilerplate
  • MVIKotlin:  
  • Uniflow-KT:  The whole framework was created to reduce boilerplate

State

Reactive State

Info

All state management libraries have a way to observe states, and this shows the function calls needed to subscribe to that state.

  • Ballast:  vm.observeStates()
  • Redux:  store.subscribe() or 3rd-party libraries
  • Orbit:  container.stateFlow
  • MVIKotlin:  store.states(Observer<State>)
  • Uniflow-KT:  onStates(viewModel) { }

Get State Snapshot

Info

Since MVI is by nature reactive, not all libraries offer an option to just query it for the current state at a given point in time. This section shows how to get a state snapshot if it is available.

  • Ballast:  vm.observeStates().value
  • Redux:  store.getState()
  • Orbit:  container.stateFlow.value
  • MVIKotlin:
  • Uniflow-KT:

State Immutability

Info

One of the big requirements for the MVI model to work properly is an immutable state class. If you can mutate the properties of the state in any way other than dispatching an Intent, then the whole model breaks down. This section explains how each library achieves immutability.

  • Ballast:  Built-in with Kotlin data class
  • Redux:  Requires Redux Toolkit w/ Immer
  • Orbit:  Built-in with Kotlin data class
  • MVIKotlin:  Built-in with Kotlin data class
  • Uniflow-KT:  Built-in with Kotlin data class

Update State

Info

This section shows the DSL methods used to update the state. Redux-style updates the state as part of the Reducer's function signature, which always returns the updated state. MVVM+ style provides a privileged scope during the handling of an Intent, which allows you to call a method to update the state.

  • Ballast:  updateState { }
  • Redux:  Reducers
  • Orbit:  reduce { }
  • MVIKotlin:  Reducer<State, Intent>
  • Uniflow-KT:  setState { }

Restore Saved States

Info

Sometimes you may need to destroy and recreate a ViewModel, and it is convenient to have a way to restore the previous state of that ViewModel without needing to do a full data refresh. This shows how this could be achieved with each library.

  • Ballast:  Saved State module
  • Redux:  
  • Orbit:  Built-in
  • MVIKotlin:  Manual restoration with Essenty
  • Uniflow-KT:  Only supports Android SavedStateHandle

Lifecycle Support

Info

Applications usually have some concept of a "lifecycle", where screens, scopes, and other features are constructed and torn down automatically by the framework. Ideally, you'd like your ViewModels to respect that lifecycle and prevent changes from being sent to the UI when it is not able to receive them. This section shows how you would tie your ViewModel's valid lifetime into the platform's Lifecycle.

  • Ballast:  Controlled by CoroutineScope
  • Redux:  
  • Orbit:  Controlled by Android ViewModel
  • MVIKotlin:  Manual control with Essenty/Binder utilities
  • Uniflow-KT:  Controlled by Android ViewModel

Automatic View-Binding

Info

One can naively understand the MVI model as a way to automatically apply data to the UI. In reality this description is more accurate to the MVVM model, but regardless, some libraries offer specificly-tailed integrations into the UI to reduce boilerplate and blur the line between MVVM and MVI.

  • Ballast:  Views observe State directly
  • Redux:  Integrates very well with React
  • Orbit:  Views observe State directly
  • MVIKotlin:  Optional MviView utility
  • Uniflow-KT:  Views observe State directly

Non-UI State Management

Info

State Management at its core is not concerned about UI, it's just concerned about data. And there's a lot of other data in your application that would do well to be managed in the same way as your UI state. This section shows which libraries have special support or documentation for managing non-UI state.

Intents

Create Intent

Info

Some MVI libraries have strict rules around creating Intents, while others are a bit more relaxes, or maybe even handle everything internally. This section shows how to create an Intent object.

  • Ballast:  Input sealed subclass constructor
  • Redux:  "actionCreators" functions
  • Orbit:  Implicit, fun vmAction() = intent { }
  • MVIKotlin:  Input sealed subclass constructor
  • Uniflow-KT:  Implicit, fun vmAction = action { }

Send Intent to VM

Info

This shows how one would dispatch an Intent into the library for eventual processing.

  • Ballast:  vm.send(Input)/vm.trySend(Input)
  • Redux:  store.dispatch()
  • Orbit:  Directly call VM function
  • MVIKotlin:  store.accept(Intent)
  • Uniflow-KT:  Directly call VM function

Asynchronous processing

Async Foreground Computation

Info

Foreground computations block the Intent processing queue, allowing long-running work to be completed and then directly update the state before another Intent starts processing.

  • Ballast:  Built-in with Coroutines
  • Redux:  
  • Orbit:  Built-in with Coroutines
  • MVIKotlin:  
  • Uniflow-KT:  Built-in with Coroutines

Async Background Computation

Info

Background computations do not block the main Intent queue and run in parallel to the ViewModel, but also cannot directly update the state. Background jobs run in parallel to the ViewModel and send their own Intents, which will get processed just as if the Intent were generated by the user.

Background computations should also be bound by the same lifecycle as the ViewModel (if supported), so that these jobs do not leak and continue running beyond the ViewModel's ability to process the changes it submits.

  • Ballast:  sideJob(key) { }
  • Redux:  "Thunk" middleware
  • Orbit:  repeatOnSubscription { }
  • MVIKotlin:  Executors+Messages
  • Uniflow-KT:  Background work launched directly in Android viewModelScope. onFlow utility for processing Flows

One-Time Notifications

Send one-off Notifications

Info

Sending events that should only be handled once is not strictly part of the MVI model, but it can be a very useful feature for integrating a state management library into an older, imperative UI toolkit. This section shows how to send these notifications from each library which supports it.

  • Ballast:  postEvent()
  • Redux:  
  • Orbit:  postSideEffect()
  • MVIKotlin:  publish(Label)
  • Uniflow-KT:  sendEvent()

React to one-off Notifications

Info

If the library is capable of sending one-off notifications, this section shows how to register your application to react to those notifications.

  • Ballast:  vm.attachEventHandler(EventHandler)
  • Redux:  
  • Orbit:  container.sideEffectFlow.collect { }
  • MVIKotlin:  store.labels(Observer<Label>)
  • Uniflow-KT:  onEvents(viewModel) { }

Additional Features

Visual Debugging

Info

One of the great features of the MVI model is the ability to capture states and Intents and send them elsewhere. A visual debugger is a great tool for capturing this activity and displaying it in a standalone UI that is not part of your application, so you can inspect (or even change) the data being inspected.

Automatic Logging

Info

Similar to Visual Debugging, it can be helpful to print the activity of your ViewModels to the application's logs so you can see the full history at a glance. This section shows which libraries support automatic logging, or whether you would need to manually wrap the library to handle it yourself.

  • Ballast:  
  • Redux:  
  • Orbit:  
  • MVIKotlin:  
  • Uniflow-KT:  

Testing Framework

Info

State management is all about managing data and doing it predictably, even though the processing is typically done asynchronously. This section shows how one would validate that their Intents are being processed correctly.