Documentation

@store

Create global state singletons that persist across screens.

Basic Usage

// src/stores/AppSettings.wh
@store object AppSettings {
  var darkMode = false
  var language = "en"
}

// Use anywhere in your app
<Switch
  bind:checked={AppSettings.darkMode}
  label="Dark Mode"
/>

How It Works

Stores are global singletons backed by StateFlow:

  • Survives screen navigation
  • Lives for the app lifetime
  • NOT tied to ViewModel lifecycle
  • Reactive - UI updates automatically

Authentication Store

// src/stores/Auth.wh
@store object Auth {
  var token: String? = null
  var user: User? = null
  var isLoggedIn = false

  suspend fun login(email: String, password: String) {
    val response = $fetch.post(
      url = "https://api.example.com/login",
      body = mapOf("email" to email, "password" to password)
    )
    token = response.token
    user = response.user
    isLoggedIn = true
  }

  fun logout() {
    token = null
    user = null
    isLoggedIn = false
  }
}

// Use in any screen
@if (Auth.isLoggedIn) {
  <Text>Welcome, {Auth.user?.name}!</Text>
  <Button onClick={Auth.logout} text="Logout" />
} else {
  <Button onClick={() => $navigate("/login")} text="Login" />
}

Cart Store

@store object Cart {
  var items: List<CartItem> = []

  val total: Double get() = items.sumOf { it.price * it.quantity }
  val itemCount: Int get() = items.sumOf { it.quantity }

  fun add(product: Product) {
    val existing = items.find { it.id == product.id }
    if (existing != null) {
      existing.quantity++
    } else {
      items = items + CartItem(product.id, product.name, product.price, 1)
    }
  }

  fun remove(productId: String) {
    items = items.filter { it.id != productId }
  }

  fun clear() {
    items = []
  }
}

// Use in product list
<Button onClick={() => Cart.add(product)} text="Add to Cart" />

// Use in cart screen
<Column>
  <Text>Items: {Cart.itemCount}</Text>
  <Text>Total: ${Cart.total}</Text>
  <Button onClick={Cart.clear} text="Clear Cart" />
</Column>

Derived State

Use get() for computed properties:

@store object UserPrefs {
  var firstName = ""
  var lastName = ""

  val fullName: String get() = "$firstName $lastName"
  val initials: String get() = "${firstName.firstOrNull() ?: ""}${lastName.firstOrNull() ?: ""}"
}

<Text>{UserPrefs.fullName}</Text>
<Text>{UserPrefs.initials}</Text>

Persistence

Stores don't persist automatically. Add persistence manually:

@store object Settings {
  var darkMode = false
  var notifications = true

  suspend fun load() {
    // Load from SharedPreferences or DataStore
    darkMode = prefs.getBoolean("dark_mode", false)
    notifications = prefs.getBoolean("notifications", true)
  }

  suspend fun save() {
    prefs.edit {
      putBoolean("dark_mode", darkMode)
      putBoolean("notifications", notifications)
    }
  }
}

// In app startup
$onMount {
  Settings.load()
}

Store vs ViewModel

@storeViewModel
LifetimeApp lifetimeScreen/navigation lifetime
ScopeGlobalPer-screen
Use CaseAuth, cart, settingsScreen-specific state
Survives rotation
Survives navigation

Multiple Stores

// src/stores/Auth.wh
@store object Auth { ... }

// src/stores/Cart.wh
@store object Cart { ... }

// src/stores/Theme.wh
@store object Theme { ... }

// Use together
@if (Auth.isLoggedIn) {
  <Text>Cart: {Cart.itemCount} items</Text>
}

<Switch
  bind:checked={Theme.darkMode}
  label="Dark Mode"
/>

Case Insensitive

@store object Settings { }
@Store object Settings { }
@STORE object Settings { }

Stores are singletons with app-wide lifetime. Don't store screen-specific state here - use ViewModels or local state instead.

Complete Example

// src/stores/Player.wh
@store object Player {
  var currentTrack: Track? = null
  var isPlaying = false
  var position: Int = 0
  var duration: Int = 0

  val progress: Float get() =
    if (duration > 0) position.toFloat() / duration else 0f

  fun play(track: Track) {
    currentTrack = track
    isPlaying = true
    position = 0
    // Start playback
  }

  fun pause() {
    isPlaying = false
  }

  fun resume() {
    isPlaying = true
  }

  fun seek(pos: Int) {
    position = pos
  }
}

// Mini player (shown on all screens)
@if (Player.currentTrack != null) {
  <Card onClick={() => $navigate("/player")}>
    <Row class="gap-12">
      <Image src={Player.currentTrack.artwork} w={48} h={48} />
      <Column>
        <Text>{Player.currentTrack.title}</Text>
        <Text>{Player.currentTrack.artist}</Text>
      </Column>
      <Button
        icon={Player.isPlaying ? "pause" : "play"}
        onClick={() =>
          Player.isPlaying ? Player.pause() : Player.resume()
        }
      />
    </Row>
  </Card>
}

See Also