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
| @store | ViewModel | |
|---|---|---|
| Lifetime | App lifetime | Screen/navigation lifetime |
| Scope | Global | Per-screen |
| Use Case | Auth, cart, settings | Screen-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
- @prop - Component properties
- $data / $layoutData - Screen data loading