Guides

Observables and view-models on Android

Cross-platform support of the @Observable macro

In this article, you will learn how swift4j leverages the power of the @Observable macro on the Android platform, which drastically increases the amount of Swift code shared between platforms. swift4j not only makes observation available to Java/Kotlin code, but also generates ready-to-use Android view-models that allow you to share UI logic automatically and focus on the platform-specific presentation.

Introduction

Modern app development relies on separating data, logic, and UI to make code easier to maintain and scale. Patterns like MVC, MVP, and especially MVVM help achieve this. MVVM is currently the state-of-the art pattern in mobile apps that lets you build responsive UIs that react to data changes and user actions.

Model–View–ViewModel

Model–View–ViewModel (MVVM) separates UI rendering (View) from state and business logic (ViewModel). The ViewModel exposes observable state and operations, while the View binds to that state and reacts to changes. This separation makes UI code simpler to reason about, easier to test, and more reusable across platforms.

MVVM (Model–View–ViewModel) is implemented on both Apple and Android platforms. On Apple, SwiftUI with Combine or the Observation framework provides MVVM support. On Android, Jetpack Compose uses LiveData, StateFlow, or similar observable types to connect ViewModels with the UI.

The Model and ViewModel layers—responsible for core data, business logic, and UI state — make up the bulk of an app’s code and can be effectively shared across platforms. To achieve this, swift4j connects Swift’s Observation framework (used for observable ViewModels on Apple platforms) with the combination of the ViewModel and StateFlow from the Android Jetpack. This means you can write your ViewModel logic once in Swift, using the familiar @Observable macro, and have swift4j generate corresponding Android ViewModel classes that expose observable state to Jetpack Compose. This approach lets you maximize code sharing and maintain a single source of truth for your app’s UI logic across both Apple and Android platforms.

Model-View-ViewModel on Apple

On Apple platforms, Swift’s Observation framework provides a lightweight, compile-time way to make ViewModels observable without hand-written plumbing.

Swift’s @Observable macro marks a type as observable. It synthesizes the storage and change notifications needed so views (and other consumers) can react when properties change—perfect for the ViewModel layer in MVVM.

The following example presents a minimal example of a ViewModel that notifies every time the counter is incremented.

import Observation

@Observable
final class Counter {
  var count: Int = 0

  func increment() { count += 1 }
}

You can also observe changes manually using withObservationTracking to understand what SwiftUI does under the hood:

import Observation

let counter = Counter()

withObservationTracking {
  _ = counter.count   // reading tracks this dependency
} onChange: {
  print("count changed")
}

counter.increment()     // prints: "count changed"

SwiftUI uses the same observation mechanism internally. Referencing an observable property in a view’s body establishes a dependency; when it changes, the view invalidates and re-renders:

import SwiftUI
import Observation

struct ContentView: View {
  @State private var counter = Counter()

  var body: some View {
    VStack {
      Text("Count: \(counter.count)")
      Button("Increment") { counter.increment() }
    }
  }
}

Model-View-ViewModel on Android

On Android, Jetpack Compose and the ViewModel architecture component provide a similar pattern for managing and observing UI state. Here’s how you can implement the same counter example using Kotlin, Jetpack ViewModel, and Compose:

import androidx.compose.runtime.*

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

class Counter : ViewModel() {
  // Compose's mutable state for observation
  var count by mutableStateOf(0)
    private set

  fun increment() {
    count += 1
  }
}

@Composable
fun CounterScreen(counter: Counter = viewModel()) {
  Column {
    Text(text = "Count: ${counter.count}")
    Button(onClick = { counter.increment() }) {
      Text("Increment")
    }
  }
}

In this example, count is a mutableStateOf property, so Compose automatically observes it and recomposes the UI when it changes. The CounterScreen composable displays the count and provides a button to increment it, mirroring the SwiftUI example from earlier.

Swift's @Observable meets Kotlin's ViewModel

swift4j integrates Swift's observables into Java/Kotlin in two main ways:

  1. Direct Observation Tracking:
    It enables direct use of observation tracking from Swift in Java/Kotlin code, without requiring Android ViewModels or Jetpack Compose. This approach offers flexibility, allowing you to observe and react to state changes in any Java/Kotlin environment—including non-Android platforms or alternative UI frameworks.

  2. Automatic Jetpack ViewModel Generation:
    For seamless Android integration, swift4j generates Android Jetpack ViewModel classes for each Swift @Observable type. These ViewModels expose observable properties as StateFlows, making it straightforward to bind them to Jetpack Compose UIs and leverage the full MVVM pattern on Android.

This dual approach lets you choose the integration style that best fits your project—either lightweight observation for any Java/Kotlin codebase, or full-featured ViewModels for Compose-based Android apps.

Direct Observation Tracking

Similarly to the use of withObservationTracking on Apple platform, swift4j generates for each observable property of a class, an additional getter with withObservationTracking prefix. For example, for the example above, beside the getCount() getter it generates an additional getCountWithObservationTracking getter accepting a lambda that will be called as soon as the property is about to be changed.

Consider the following Swift class from the previous example, that is exported by adding the @jvmmacro:

import Observation
import Swift4j

@jvm
@Observable
final class Counter {
  var count: Int = 0
  func increment() { count += 1 }
}

swift4j bridges this class such that it can be accessed from Kotlin like this:

val counter = Counter()

val count = counter.getCountWithObservationTracking {
  print("count changed")
}

println("Current count: $count")
counter.increment()     // prints: "count changed"

All getters with withObservationTracking preserve the same semantics as in Apple and hence have following properties that have to be noticed:

  • the observation callback is called before the value is changed
  • it's called only once and after a change, a new tracking has to be registered

Because of this, in order to have continious tracking and get new values together with the notification some extra work has to be done. For more details on how it can be achieved, you can take a look at the observation example in swift4j-examples.

Automatic Jetpack ViewModel Generation

In order to start using Swift view models immediately from Kotlin without the limitations or extra work mentioned in the previous section, one can use auto-generated Jetpack ViewModels that can be bound directly to Jetpack Compose.

For the previous example,

Consider the example from the previous section. swift4j automatically generates a ViewModel class named CounterViewModel. Additionally, it generates an extension with a function viewModel to create a CounterViewModel from the Counter class and a factory CounterViewModelFactory. Both are used to correctly integrate the view models into the Android application lifecycle.

The generated ViewModel class contains all observable properties as instances of Jetpack StateFlow, which allow collecting each property as a Jetpack Compose State.

The following example shows how we can use the CounterViewModel in Compose:

class MainActivity : ComponentActivity() {
    // Create and register the viewModel within the Activity
    private val counterVm: CounterViewModel by viewModels {        
        CounterViewModelFactory(Counter())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
      setContent {
        CounterScreen(counterVm)
      }
    }
}

@Composable
fun CounterScreen(counterVm: CounterViewModel) {
  
  // Collect the "count" StateFlow as a state
  val count by viewModel.count.collectAsState()

  Column {
    Text(text = "Count: $count")
    Button(onClick = { counterVm.model.increment() }) {
      Text("Increment")
    }
  }
}

The full demo project for Android Studio of the presented example can be found at swift4j-examples/Android/ObservationDemo with the corresponding Swift Package swift4j-examples/Packages/ObservationDemo. To build and run the demo project, clone the examples repository swift4j-examples and open the Android Studio project located at swift4j-examples/Android/ObservationDemo.

Conclusion

Using Swift's @Observable macro together with swift4j lets you implement ViewModel logic once in Swift and reuse it on Android—either by exposing direct observation hooks to Java/Kotlin or by generating full Jetpack ViewModels. This reduces duplication, improves testability, and simplifies cross-platform UI state management. See the linked examples to try the integration end-to-end.

Observables and view-models on Android | Scade.io