Ballast Core

Overview

The Ballast Core module provides all the core capabilities of the entire Ballast MVI framework. The Core framework is robust and opinionated, but also provides many ways to extend the functionality through Interceptors without impacting the core MVI model. Any additional functionality outside of Core will typically be implemented as an Interceptor and provided to the BallastViewModelConfiguration.

Usage

ViewModels

The Core module provides several ViewModel base classes, so Ballast can integrate natively with a variety of platforms.

  • AndroidViewModel: A subclass of androidx.lifecycle.ViewModel
  • IosViewModel: A custom ViewModel that can be integrated with Combine Publishers for SwiftUI
  • BasicViewModel: A generic ViewModel for Kotlin targets that don't have their own platform-specific ViewModel, or for anywhere you want to manually control the lifecycle of the ViewModel. BasicViewModel's lifecycle is controlled by a coroutineScope provided to it upon creation. When the scope gets cancelled, the ViewModel gets closed and can not be used again.

Interceptors

The Core module comes with only one Interceptor,

  • LoggingInterceptor: It will print all Ballast activity to the logger provided in the BallastViewModelConfiguration. The information logged by this interceptor may be quite verbose, but it can be really handy for quickly inspecting the data in your ViewModel and what happened in what order.

The LoggingInterceptor writes to a logger installed into the BallastViewModelConfiguration, which may be used by InputHandlers or other Ballast features as well.

Ballast offers several logger implementations out-of-the-box:

  • NoOpLogger: The default implementation, it simply drops all messages and exceptions so nothing gets logged accidentally. It's recommended to use this in production builds.
  • PrintlnLogger: Useful for quick-and-dirty logging on all platforms. It just writes log messages to stdout through println.
  • AndroidLogger: Only available on Android, writes logs to the default LogCat at the appropriate levels.
  • JsConsoleLogger: Only available on JS, writes logs to console.log() or console.error()
  • NSLogLogger: Only available on iOS, writes logs to NSLog
  • OSLogLogger: Only available on iOS, writes logs to OSLog

Danger

Be sure to only include LoggingInterceptor() and the logger in debug builds, as logging in production may cause performance degradation and risks leaking sensitive info through to the application logs. It should not be used to create a paper-trail of activity in your app, you should use something like Ballast Analytics to more selectively create the paper-trail.

class ExampleViewModel(coroutineScope: CoroutineScope) : BasicViewModel<
    ExampleContract.Inputs,
    ExampleContract.Events,
    ExampleContract.State>(
    coroutineScope = coroutineScope,
    config = BallastViewModelConfiguration.Builder()
        .apply {
            if(DEBUG) { // some build-time constant
                logger = PrintlnLogger()
                this += LoggingInterceptor()
            }
        }
        .withViewModel(
            initialState = ExampleContract.State(),
            inputHandler = ExampleInputHandler(),
            name = "Example",
        )
        .build(),
    eventHandler = ExampleEventHandler(),
)

Input Strategies

Ballast offers 3 different Input Strategies out-of-the-box, which each adapt Ballast's core functionality for different applications:

  • LifoInputStrategy: A last-in-first-out strategy for handling Inputs, and the default strategy if none is provided. Only 1 Input will be processed at a time, and if a new Input is received while one is still working, the running Input will be cancelled to immediately accept the new one. Corresponds to Flow.collectLatest { }, best for UI ViewModels that need a highly responsive UI where you do not want to block the user's actions.
  • FifoInputStrategy: A first-in-first-out strategy for handling Inputs. Inputs will be processed in the same order they were sent and only ever one-at-a-time, but instead of cancelling running Inputs, new ones are queued and will be consumed later when the queue is free. Corresponds to the normal Flow.collect { }, best for non-UI ViewModels, or UI ViewModels where it is OK to "block" the UI while something is loading.
  • ParallelInputStrategy: For specific edge-cases where neither of the above strategies works. Inputs are all handled concurrently so you don't have to worry about blocking the queue or having Inputs cancelled. However, it places additional restrictions on State reads/changes to prevent usage that might lead to race conditions.

Danger

For historical reasons, LifoInputStrategy is the default, but can be unintuitive to work with and cause subtle issues in your application. For this reason, it is recommended to manually choose to use FifoInputStrategy unless you are familiar enough with Ballast and it's workflow to understand the full implications LifoInputStrategy.

This default input strategy will likely be changed to FifoInputStrategy in a future version, so it would be best to start by explicitly choosing the strategy you wish to use for every ViewModel, rather than relying on the default or having your application start behaving differently in a future version of Ballast.

InputStrategies are responsible for creating the Channel used to buffer incoming Inputs, consuming the Inputs from that channel, and providing a "Guardian" to ensure the Inputs are handled properly according the needs of that particular strategy. The DefaultGuardian is a good starting place if you need to create your own InputStrategy to maintain the same level of safety as the core strategies listed above.

Info

Pro Tip: The text descriptions of these InputStrategies can be a bit confusing, but seeing them play out in real-time should make it obvious how they work. Playing with the Kitchen Sink example while using the Debugger gives you a simple way of experiencing these behaviors to get an intuition for when to use each one.

Installation

repositories {
    mavenCentral()
}

// for plain JVM or Android projects
dependencies {
    implementation("io.github.copper-leaf:ballast-core:4.0.0")
}

// for multiplatform projects
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.github.copper-leaf:ballast-core:4.0.0")
            }
        }
    }
}