{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Arda Kazancı — Engineering Journal",
  "home_page_url": "https://ardakazanci.com/",
  "feed_url": "https://ardakazanci.com/feed.json",
  "description": "Staff Android Engineer sharing practical notes on Android architecture, Kotlin, Coroutines, Jetpack Compose, and AndroidX internals.",
  "icon": "https://ardakazanci.com/apple-touch-icon.png",
  "language": "en",
  "authors": [
    {
      "name": "Arda Kazancı",
      "url": "https://ardakazanci.com/about/"
    }
  ],
  "items": [
    {
      "id": "https://ardakazanci.com/posts/savedstatehandle-savedstatecontainer/",
      "url": "https://ardakazanci.com/posts/savedstatehandle-savedstatecontainer/",
      "external_url": "https://ardakazanci.com/posts/savedstatehandle-savedstatecontainer.md",
      "title": "Nested SavedStateHandles: Isolating Child State Without Key Prefixes",
      "summary": "Isolate reusable state holders inside a single ViewModel with nested SavedStateHandles, SavedStateContainer integration, and the new StateFlow-LiveData rules.",
      "content_text": "Suppose a checkout screen has one `CheckoutViewModel` managing two address forms: shipping and billing. Both forms use the same `AddressFormStateHolder` class, and both need their text inputs to survive process death.\n\nUntil now, a `ViewModel` received a single flat `SavedStateHandle`. Passing that same handle to two instances of `AddressFormStateHolder` meant both wrote to `\"street\"` and `\"city\"`, overwriting each other's state.\n\nThe usual workaround was string prefixing: pass `\"shipping\"` and `\"billing\"` into the constructor, then build keys like `\"${prefix}_street\"` and `\"${prefix}_city\"` by hand. That works, though every reusable state holder has to invent its own naming scheme and hope no parent key collides with it.\n\nAndroidX Lifecycle commit [`8f4994031e712166d1df58a75f9dae2669424e95`](https://github.com/androidx/androidx/commit/8f4994031e712166d1df58a75f9dae2669424e95) solves this directly by migrating `SavedStateHandle` onto `androidx.savedstate.SavedStateContainer` and introducing nested handles.\n\n## What SavedStateHandle adds\n\nThe change in `lifecycle-viewmodel-savedstate` adds three `@MainThread` functions to `SavedStateHandle`:\n\n```kotlin title=\"SavedStateHandle.kt\"\n@MainThread public fun asContainer(): SavedStateContainer\n\n@MainThread public fun createOrGetContainer(key: String): SavedStateContainer\n\n@MainThread public fun createOrGetSavedStateHandle(key: String): SavedStateHandle\n```\n\n`asContainer()` exposes the `SavedStateContainer` backing the current handle. It also replaces the old `@RestrictTo(LIBRARY_GROUP)` `savedStateProvider()` method that Lifecycle used internally when bundling ViewModel state.\n\n`createOrGetContainer(key)` returns an existing nested `SavedStateContainer` registered under `key`, or creates and registers a new child container on first access. `createOrGetSavedStateHandle(key)` wraps that child container in a new `SavedStateHandle`.\n\nOne detail is worth noting if you read the commit message alongside the source: the draft `RelNote` mentions `getSavedStateHandle()`, while the checked-in public API in [`SavedStateHandle.kt`](https://github.com/androidx/androidx/blob/8f4994031e712166d1df58a75f9dae2669424e95/lifecycle/lifecycle-viewmodel-savedstate/src/commonMain/kotlin/androidx/lifecycle/SavedStateHandle.kt) and [`api/current.txt`](https://github.com/androidx/androidx/blob/8f4994031e712166d1df58a75f9dae2669424e95/lifecycle/lifecycle-viewmodel-savedstate/api/current.txt) is `createOrGetSavedStateHandle(key: String)`. Because these details come from an upstream development commit, method names and behavior can still change before a stable release.\n\n## Child state holders without key prefixes\n\nWith a flat `SavedStateHandle`, a reusable form holder had to carry a caller-supplied prefix just to stay out of its neighbor's way:\n\n```kotlin title=\"PrefixedAddressFormStateHolder.kt\"\nclass PrefixedAddressFormStateHolder(\n    private val savedStateHandle: SavedStateHandle,\n    prefix: String,\n) {\n    private val streetKey = \"${prefix}_street\"\n    private val cityKey = \"${prefix}_city\"\n\n    val street: StateFlow<String> = savedStateHandle.getStateFlow(streetKey, \"\")\n    val city: StateFlow<String> = savedStateHandle.getStateFlow(cityKey, \"\")\n\n    fun onStreetChanged(value: String) {\n        savedStateHandle[streetKey] = value\n    }\n\n    fun onCityChanged(value: String) {\n        savedStateHandle[cityKey] = value\n    }\n}\n```\n\nWith `createOrGetSavedStateHandle`, the child holder owns an isolated namespace. Plain keys are safe again:\n\n```kotlin title=\"AddressFormStateHolder.kt\"\nclass AddressFormStateHolder(\n    private val savedStateHandle: SavedStateHandle,\n) {\n    val street: StateFlow<String> = savedStateHandle.getStateFlow(\"street\", \"\")\n    val city: StateFlow<String> = savedStateHandle.getStateFlow(\"city\", \"\")\n\n    fun onStreetChanged(value: String) {\n        savedStateHandle[\"street\"] = value\n    }\n\n    fun onCityChanged(value: String) {\n        savedStateHandle[\"city\"] = value\n    }\n}\n```\n\nThe parent `ViewModel` decides how each child is scoped in its saved state tree:\n\n```kotlin title=\"CheckoutViewModel.kt\"\nclass CheckoutViewModel(\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n\n    val shippingAddress = AddressFormStateHolder(\n        savedStateHandle.createOrGetSavedStateHandle(\"shipping_address\")\n    )\n\n    val billingAddress = AddressFormStateHolder(\n        savedStateHandle.createOrGetSavedStateHandle(\"billing_address\")\n    )\n}\n```\n\nBoth forms use the exact same class and key names without colliding. When Android saves the activity or navigation entry state, the parent container serializes `\"shipping_address\"` and `\"billing_address\"` as nested `SavedState` bundles. Calling `createOrGetSavedStateHandle(\"shipping_address\")` a second time attaches to the same underlying child `SavedStateContainer`, so both handles read and write the same values.\n\nAll three methods are annotated with `@MainThread`, matching the rest of `SavedStateHandle`. Create child handles during `ViewModel` initialization on the main thread or from `Dispatchers.Main.immediate`.\n\n## Passing containers to non-ViewModel components\n\nSome state holders avoid depending on `lifecycle-viewmodel-savedstate` so they can stay usable outside ViewModels or in lower-level multiplatform modules that only pull in `androidx.savedstate`. `createOrGetContainer` and `asContainer` bridge that boundary without custom `SavedStateProvider` glue:\n\n```kotlin title=\"CatalogViewModel.kt\"\nclass CatalogFilterController(\n    private val container: SavedStateContainer,\n) {\n    // Manages filter state directly through androidx.savedstate APIs\n}\n\nclass CatalogViewModel(\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n\n    // Isolated sub-scope under \"catalog_filters\"\n    val filters = CatalogFilterController(\n        savedStateHandle.createOrGetContainer(\"catalog_filters\")\n    )\n\n    // Shares the ViewModel's root container when isolation isn't needed\n    val rootContainer: SavedStateContainer = savedStateHandle.asContainer()\n}\n```\n\nContainers can nest further too. The upstream tests verify calling `childContainer.createOrGetContainer(\"subNested\")`, so a feature controller handed a `SavedStateContainer` can carve out its own child scopes without ever touching a `ViewModel` or `SavedStateHandle`.\n\n## Why getStateFlow and getLiveData now conflict\n\nThis migration includes one deliberate breaking behavior change: **calling `getStateFlow` and `getLiveData` with the same key now throws an `IllegalArgumentException`.**\n\nPreviously, `getMutableStateFlow` and `getLiveData` were already mutually exclusive on the same key, while read-only `getStateFlow(key, initialValue)` lived in a separate `flows` map. Because of that extra map, you could observe a key through `getStateFlow` and still mutate it through `getLiveData` (updating the `LiveData` pushed the new value into `flows[key]` as well).\n\nThat special case is gone. `getStateFlow(key, initialValue)` now delegates directly to `getMutableStateFlow(key, initialValue).asStateFlow()`. Accessing the same key through both APIs fails immediately, regardless of call order. [SavedStateHandleTest.kt](https://github.com/androidx/androidx/blob/8f4994031e712166d1df58a75f9dae2669424e95/lifecycle/lifecycle-viewmodel-savedstate/src/androidDeviceTest/kotlin/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt).\n\nCodebases partway through a `LiveData`-to-`StateFlow` migration are the most likely place to trip over this:\n\n```kotlin title=\"SearchViewModel.kt\"\nclass SearchViewModel(\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n\n    // Broken after this change: throws IllegalArgumentException for \"query\"\n    // val queryLiveData = savedStateHandle.getLiveData(\"query\", \"\")\n    // val queryFlow = savedStateHandle.getStateFlow(\"query\", \"\")\n\n    // Fix: keep one saved backing source and derive the second stream\n    private val mutableQuery: MutableStateFlow<String> =\n        savedStateHandle.getMutableStateFlow(\"query\", \"\")\n\n    val queryFlow: StateFlow<String> = mutableQuery.asStateFlow()\n    val queryLiveData: LiveData<String> = mutableQuery.asLiveData()\n\n    fun onQueryChanged(newQuery: String) {\n        mutableQuery.value = newQuery\n    }\n}\n```\n\nPick one saved state representation per key. If an older XML fragment or observer still expects `LiveData`, expose `.asLiveData()` from the `StateFlow` instead of registering two saved wrappers on `\"query\"`.\n\n## What changed inside SavedStateHandleImpl\n\nLooking inside [`SavedStateHandleImpl.kt`](https://github.com/androidx/androidx/blob/8f4994031e712166d1df58a75f9dae2669424e95/lifecycle/lifecycle-viewmodel-savedstate/src/commonMain/kotlin/androidx/lifecycle/internal/SavedStateHandleImpl.kt) and [`SavedStateHandle.android.kt`](https://github.com/androidx/androidx/blob/8f4994031e712166d1df58a75f9dae2669424e95/lifecycle/lifecycle-viewmodel-savedstate/src/androidMain/kotlin/androidx/lifecycle/SavedStateHandle.android.kt) makes both the new nesting APIs and the `LiveData` restriction easy to understand.\n\nBefore this commit, `SavedStateHandle` maintained five separate maps across common and Android source sets: `regular`, `providers`, `flows`, `mutableFlows`, and `liveDatas` (backed by a custom `SavingStateLiveData` subclass). Every `set()`, `remove()`, and state-saving pass had to synchronize those maps by hand.\n\nNow `SavedStateHandleImpl` wraps a single `SavedStateContainer`. Every entry in that container is stored as an internal `SavedStateValue<T>` implementation, bundled under the `\"androidx.lifecycle.savedstate.value\"` key when saved:\n\n- `SimpleSavedStateValue<T>` holds plain `handle[\"key\"] = value` entries.\n- `StateFlowSavedStateValue<T>` holds a `MutableStateFlow<T>` for both `getStateFlow` and `getMutableStateFlow`.\n- `LiveDataSavedStateValue<T>` (Android-only) wraps `MutableLiveData<T>`, replacing `SavingStateLiveData`.\n- `SavedStateProviderSavedStateValue` wraps registered `SavedStateProvider` callbacks.\n\nOne key maps to one `SavedStateValue` slot in the container. There is nowhere left for a parallel `LiveData` and `StateFlow` pair to live under the same key.\n\nTwo edge cases in the diff show how backwards compatibility was preserved during this rewrite.\n\nRight after process recreation, `SavedStateHandle.createHandle` populates the root container before your `ViewModel` constructor runs. At that moment, Lifecycle cannot tell whether `\"shipping_address\"` was a plain `Bundle` value or a child container, so the restored entry initially lands in the parent container as a `SimpleSavedStateValue` holding a `SavedState` object. When your code later calls `createOrGetContainer(\"shipping_address\")` (directly or through `createOrGetSavedStateHandle`), `SavedStateHandleImpl` spots that `SimpleSavedStateValue`, removes the placeholder, creates the real child `SavedStateContainer`, and unpacks the restored key-value pairs into it. Even if you create a child state holder lazily when the user expands an optional form section, its restored state is still there.\n\n`contains(key)` also needed a guard on Android for uninitialized `LiveData`. Calling `handle.getLiveData<String>(\"draft\")` with no initial value registers a `LiveDataSavedStateValue` in the container right away. Checking `key in impl` alone would have made `\"draft\" in handle` return `true` before any value was set. `SavedStateHandle.android.kt` checks `savedStateValue.value.isInitialized` whenever the stored wrapper is `LiveDataSavedStateValue`, keeping `contains()` consistent with earlier releases.\n\n## Practical takeaways before adopting it\n\nWhen this change ships in an upcoming `lifecycle-viewmodel-savedstate` alpha, audit existing ViewModels for any string key shared between `getLiveData` and `getStateFlow` before bumping the version. The compiler cannot catch duplicate string keys, and the failure only triggers at runtime when the second accessor runs.\n\nFor new screens, `createOrGetSavedStateHandle` fits best when one `ViewModel` composes repeated form sections, multi-step wizard pages, or shared filter controllers that each persist a few fields across process death. A simple screen with two or three flat keys does not need nested handles just because the API exists. I inspected upstream commit `8f4994031e712166d1df58a75f9dae2669424e95` and its device tests; the application snippets here were written against those signatures and were not compiled against a published artifact.",
      "image": "https://ardakazanci.com/og/savedstatehandle-savedstatecontainer.png",
      "date_published": "2026-11-01T10:35:00.000Z",
      "date_modified": "2026-11-01T10:35:00.000Z",
      "tags": [
        "android",
        "kotlin",
        "architecture"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    },
    {
      "id": "https://ardakazanci.com/posts/jetpack-appstate-explained/",
      "url": "https://ardakazanci.com/posts/jetpack-appstate-explained/",
      "external_url": "https://ardakazanci.com/posts/jetpack-appstate-explained.md",
      "title": "Jetpack AppState: When Premium Ends While Music Is Playing",
      "summary": "Compare StateFlow and AppState through a music app: shared subscriptions, derived playback policies, and the roles of ViewModel and the playback service.",
      "content_text": "A song is playing. The user has put the app in the background. Then the server reports that their subscription has expired.\n\nRemoving the Premium label from the account screen is easy. The playback service also needs to know. Subsequent playback should follow the Free plan's advertising rules, with audio quality set to Medium. When the user opens the player again, it should show the same subscription status.\n\nIn this example app, the current song finishes uninterrupted. Advertising and quality changes take effect when preparing the next track. An ad does not have to play before every song; the ad scheduler decides the frequency. These are our product rules, not a claim about the exact behavior of any particular music service.\n\nThis gives us a concrete way to look at Google's AppState libraries: **where should shared application state live, and how do we derive decisions from it?**\n\n## What AppState adds\n\nAppState has two main libraries. **StateStore** holds shared Compose state behind type-safe keys. **Transform** runs a composable calculation without rendering UI and exposes its result as `State<T>`. You can use either independently. [AppState guide](https://developer.android.com/topic/libraries/architecture/appstate).\n\n**AppState is an alternative you can evaluate within your existing Android architecture.** It does not require removing ViewModels or converting working Flow pipelines. You can try Compose's state model outside the UI in one feature while keeping your existing approach elsewhere.\n\nAs of October 30, 2026, both are at `1.0.0-alpha01`. The first alpha shipped on October 28; APIs may change. [Transform releases](https://developer.android.com/jetpack/androidx/releases/appstate-transform), [StateStore releases](https://developer.android.com/jetpack/androidx/releases/appstate-statestore).\n\n## How we would build this today\n\nA shared repository can expose the subscription as a `StateFlow`. The player and account ViewModels observe it. The playback service uses the same source. Each screen does not need to maintain its own independent subscription copy.\n\n![Shared subscription and playback policy with StateFlow](/images/appstate/with-stateflow-v2.png)\n\n_The drawing focuses on subscription sharing. The code below also includes the user's preferred audio quality._\n\nPremium users can choose Medium or High. Free users get Medium. We use `UNKNOWN` until the subscription is verified, rather than silently treating an unresolved subscription as Free. A `null` playback policy means the decision is not ready yet.\n\n```kotlin title=\"PlaybackPolicy.kt\"\n@Serializable\nenum class Subscription { UNKNOWN, FREE, PREMIUM }\n\nenum class AudioQuality { MEDIUM, HIGH }\n\ndata class PlaybackPolicy(\n    val adsEnabled: Boolean,\n    val quality: AudioQuality,\n)\n```\n\nWith Flow, we can select the source to observe based on the subscription:\n\n```kotlin title=\"FlowPlaybackPolicy.kt\"\n@OptIn(ExperimentalCoroutinesApi::class)\nfun flowPlaybackPolicy(\n    subscription: StateFlow<Subscription>,\n    preferredQuality: Flow<AudioQuality>,\n    scope: CoroutineScope,\n): StateFlow<PlaybackPolicy?> = subscription\n    .flatMapLatest<Subscription, PlaybackPolicy?> { plan ->\n        when (plan) {\n            Subscription.UNKNOWN -> flowOf(null)\n            Subscription.FREE -> flowOf(\n                PlaybackPolicy(adsEnabled = true, quality = AudioQuality.MEDIUM)\n            )\n            Subscription.PREMIUM -> preferredQuality\n                .map<AudioQuality, PlaybackPolicy?> { quality ->\n                    PlaybackPolicy(adsEnabled = false, quality = quality)\n                }\n                .onStart { emit(null) }\n        }\n    }\n    .stateIn(scope, SharingStarted.Eagerly, null)\n```\n\nThe Premium branch observes the quality preference. Switching to Free makes `flatMapLatest` cancel that collection and emit the fixed Free policy. `Eagerly` is deliberate: the service's policy should not depend on a screen being subscribed. Work stops when the supplied scope ends. [flatMapLatest](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html), [stateIn](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html).\n\nThis solution meets the needs of our example. When comparing it with AppState, the useful question is which approach makes the same behavior easier to read and maintain.\n\n## Would a SharedViewModel be enough?\n\nScreens within the same activity or navigation graph can observe a subscription through a SharedViewModel. Sharing a ViewModel between an Activity and a Service in the same process is also technically possible: both can use a common `ViewModelStoreOwner` and request the same ViewModel with the same key. You then own that store's lifetime and cleanup. Passing an Activity-owned ViewModel reference to a service does not extend its lifetime. [Custom ViewModel scopes](https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis), [owner contract](https://developer.android.com/reference/androidx/lifecycle/ViewModelStoreOwner).\n\nFor this example, I would keep the service independent of an Activity-owned ViewModel. Playback may continue after the screens are gone.\n\nWe need a state owner independent of a screen. In the existing design, that can be a repository. In an AppState design, it can be a manager. ViewModel still owns screen state and user interactions. **AppState does not replace ViewModel.** [ViewModel ownership and scope](https://developer.android.com/topic/libraries/architecture/viewmodel).\n\n## Sharing the subscription with StateStore\n\nFor UI and service components in the same process, we create a shared `StateStore` through DI. `SubscriptionManager` keeps its key private and exposes read-only state. The layer receiving verified subscription updates owns writes.\n\n```kotlin title=\"SubscriptionManager.kt\"\n@Serializable\nprivate data object SubscriptionKey :\n    StateStoreKey<Subscription>(Subscription.UNKNOWN)\n\nclass SubscriptionManager(private val store: StateStore) {\n    val subscription: State<Subscription> =\n        store.getState(SubscriptionKey)\n\n    internal fun onVerifiedSubscriptionChanged(value: Subscription) {\n        store.setState(SubscriptionKey, value)\n    }\n}\n```\n\nThis method does not detect expiration by itself. Our subscription synchronization component fetches the current entitlement from the backend and passes the verified result to the manager. A refresh or an app-specific update notification can trigger that fetch. AppState supplies neither the network transport nor the billing integration.\n\nHere, `PREMIUM` means the user still has Premium access. Turning off renewal does not necessarily remove that access immediately. For Play subscriptions, cancellation can leave access until the billing period ends, and grace periods need separate handling. Our backend resolves those billing states into the app's entitlement; the client enum deliberately does not model the entire billing lifecycle. [Subscription lifecycle](https://developer.android.com/google/play/billing/lifecycle/subscriptions).\n\nWe do not create a new manager and store for every ViewModel. Sharing those instances is the DI setup's responsibility; StateStore is not automatically a singleton. A service running in another process cannot share this memory and needs IPC. [StateStore scope](https://developer.android.com/topic/libraries/architecture/appstate/statestore).\n\n![Shared subscription, Transform and playback service with AppState](/images/appstate/with-appstate.png)\n\n_StateStore holds the subscription. Transform derives playback policy. The service applies it. Additional inputs, such as the quality preference, are omitted from the drawing for clarity._\n\n## What Transform does here\n\nWe can derive the same playback policy using Compose state. The quality preference can remain a `Flow` from our existing DataStore or repository:\n\n```kotlin title=\"ComposePlaybackPolicy.kt\"\nfun composePlaybackPolicy(\n    subscriptions: SubscriptionManager,\n    preferredQuality: Flow<AudioQuality>,\n    scope: CoroutineScope,\n): State<PlaybackPolicy?> = transform<PlaybackPolicy?>(\n    initialValue = null,\n    scope = scope,\n) {\n    when (subscriptions.subscription.value) {\n        Subscription.UNKNOWN -> null\n        Subscription.FREE ->\n            PlaybackPolicy(adsEnabled = true, quality = AudioQuality.MEDIUM)\n        Subscription.PREMIUM -> {\n            val quality = preferredQuality\n                .collectAsState(initial = null).value\n            quality?.let { PlaybackPolicy(adsEnabled = false, quality = it) }\n        }\n    }\n}\n```\n\nWhile the subscription is Premium, `collectAsState` collects the quality preference. On a switch to Free, that branch leaves the composition and its collection is cancelled. The preference is no longer an input to this calculation; we return the Free policy.\n\nThe useful part is **expressing which state matters under which conditions through ordinary Kotlin control flow**. The Flow version expressed the same relationship with operators. Both are valid. Transform provides another way to combine Compose state with asynchronous sources. [Transform examples](https://developer.android.com/topic/libraries/architecture/appstate/transform).\n\nIf all we needed was a synchronous mapping from Premium to ad-free and Free to ad-supported, `derivedStateOf` could be enough. Collecting the asynchronous quality preference inside the Premium branch is what makes Transform relevant here. The difference from `combine` is not that Flow cannot do conditional work: `flatMapLatest` already does it in our first version.\n\nCreate the policy once per intended owner, rather than calling this factory whenever a value is needed. Each call starts another transformation. In this design the service owns its policy calculation, while both ViewModels read the shared subscription manager.\n\n## What happens to the song already playing?\n\nThere is no `player.stop()` or play-ad command inside the Transform block. Its output is only a policy: are ads enabled, and which quality should be used?\n\nWhen the subscription becomes Free, the UI can reflect the new status. The service lets the current track finish, then uses the current rules for ad scheduling and quality selection when preparing subsequent playback. It also needs to reconsider any next track prepared in advance. Using Medium on the Free plan does not erase the user's saved Premium quality preference.\n\nThe drawings show subscription and policy dependencies, not the complete playback-control path. In a Media3 implementation, the player and `MediaSession` belong in a `MediaSessionService`; the UI uses a `MediaController` to control playback. AppState does not replace that connection. Likewise, `AudioQuality.MEDIUM` is our app's policy value, not an AppState codec setting: the playback layer must translate it into an available stream or track selection. [Media3 background playback](https://developer.android.com/media/media3/session/background-playback).\n\nHere, **the target policy and the actual playback state are different things**. If the current track is finishing at High quality, the UI should not claim that it is already playing at Medium. Actual playback information comes from the player.\n\nTransform updates asynchronously. The service must not update the subscription and assume the derived value is fresh on the very next line. The Flow pipeline is asynchronous too. A practical implementation can serialize entitlement changes and next-track preparation through one playback coordinator. Before starting a prepared track, it checks the current entitlement and re-evaluates the policy if necessary. Both versions should use the same policy rules, not independently maintained copies.\n\nIn this example, `null` means a new playback decision is not ready yet; it does not mean Free or “reuse Premium.” We let the current track finish but wait to prepare new playback until a policy is available. The `.onStart { emit(null) }` in the Flow version makes that waiting state explicit when entering Premium and loading the quality preference.\n\n## Questions that matter outside the happy path\n\n**How does a service observe Compose state without a screen?** Reading `.value` once only reads the current value. It does not subscribe. A service can collect `snapshotFlow { policy.value }` in its own coroutine scope and update the coordinator's pending policy. Keep player commands out of the snapshot block. This bridges Compose state back into ordinary Flow collection; it does not turn state into a lossless event queue. [snapshotFlow](https://developer.android.com/develop/ui/compose/side-effects#snapshotFlow).\n\n**What if the app is offline, or its process dies?** A network failure alone is not proof that Premium ended. Offline entitlement validity needs an explicit product policy. This minimal example starts at `UNKNOWN` after process recreation and waits for verification before new playback. An application scope lasts only as long as the process; neither it nor StateStore keeps a service alive.\n\n**What if the user changes accounts?** Cancel the previous account's synchronization, reset the shared subscription to `UNKNOWN`, and reject late responses belonging to that account. A singleton manager is shared storage, not an account-isolation mechanism. This is a responsibility of our synchronization design.\n\n**Could recomposition play an ad twice?** Our calculation never plays an ad. It produces the latest policy, while the playback coordinator owns actual ad scheduling for a track transition. Billing records, ad impressions, and playback commands that must not be lost belong in explicit event-processing paths, not a history inferred from Compose state changes.\n\n## What the source code tells us\n\nI inspected AndroidX commit `2b885e14442e559fa812d4b693328bb7256c3fdc`. These development-branch details are not permanent API guarantees.\n\nStateStore uses a `SnapshotStateMap` and mutable state objects internally. `getState` exposes observable state for a key. Sharing works because consumers use the same store instance and its snapshot state. [StateStore.kt](https://github.com/androidx/androidx/blob/2b885e14442e559fa812d4b693328bb7256c3fdc/appstate/statestore/statestore/src/commonMain/kotlin/androidx/appstate/statestore/StateStore.kt).\n\nTransform creates an actual `Composition` and `Recomposer`, with an applier that does not build a UI tree. When the scope ends, the composition is cleaned up. This runs more machinery than an ordinary getter; it does not promise an automatic performance improvement. [TransformScope.kt](https://github.com/androidx/androidx/blob/2b885e14442e559fa812d4b693328bb7256c3fdc/appstate/transform/transform/src/commonMain/kotlin/androidx/appstate/transform/TransformScope.kt).\n\nThe upstream tests advance the test runtime after changing state and check that cancelling the scope stops further updates. A service-owned calculation needs a service-owned scope; a genuinely app-wide calculation needs an application scope. Using `viewModelScope` does not give it the service's lifetime. [Transform tests](https://github.com/androidx/androidx/blob/2b885e14442e559fa812d4b693328bb7256c3fdc/appstate/transform/transform/src/commonTest/kotlin/androidx/appstate/transform/TransformTest.kt).\n\nStateStore lives in memory. Disk restoration requires a separate DataStore integration. We do not persist the subscription in this example: restoring an old Premium value does not verify a current entitlement. [Persistence implementation](https://github.com/androidx/androidx/blob/2b885e14442e559fa812d4b693328bb7256c3fdc/appstate/statestore/statestore-datastore/src/commonMain/kotlin/androidx/appstate/datastore/StateStore.kt).\n\n## Would I add it to an existing app?\n\nI would try it in a small part of a Compose-based app where shared state and conditional asynchronous sources have become difficult to follow. If the domain module is independent of Compose, I would preserve that boundary and place the manager and Transform in a framework-aware layer.\n\nI would not replace a working StateFlow architecture just because a new library exists. For one shared subscription property, even a manager with `mutableStateOf` may be enough. If keyed storage or persistence integration adds no value, StateStore is optional.\n\nTo experiment, add `androidx.appstate.transform:transform:1.0.0-alpha01` and, when using the store, `androidx.appstate.statestore:statestore:1.0.0-alpha01`. Modules containing Transform blocks need the Compose compiler plugin and the documented Android Compose configuration. The example's `@Serializable` declarations also need Kotlin serialization setup. [Setup guide](https://developer.android.com/topic/libraries/architecture/appstate#project-setup).\n\nThe snippets are abbreviated: imports, DI wiring, subscription synchronization, error handling, and player integration are omitted. The quality Flow must expose a usable fallback or an explicit error state; an uncaught exception is not a loading strategy. `UNKNOWN` and `null` keep the example small, but a production app may need distinct loading, offline, and failure states.\n\nBefore shipping, I would test expiry during playback, renewal before the next track, a late response after an account switch, and cancellation when the service stops. The transition test should assert that the current track is untouched and the next preparation uses Free rules. I inspected the APIs and upstream tests; these Kotlin examples were not compiled or run in an Android project.\n\nThe important design decision in this music app is choosing a shared subscription owner and deciding when new rules take effect. StateStore and Transform let us express those decisions with Compose's state model. They do not take over playback, ViewModel ownership, or the server's authority over subscription status.\n\nMy recommendation is to evaluate AppState as an alternative in one small feature that combines Compose state with conditional asynchronous sources. If it makes the code clearer and lifecycle ownership easier to manage, it may be a useful choice. If the existing StateFlow solution already expresses the behavior clearly and reliably, keeping it is a good choice too. Let the concrete problem guide the decision, rather than the availability of a new API.",
      "image": "https://ardakazanci.com/og/jetpack-appstate-explained.png",
      "date_published": "2026-10-30T13:35:00.000Z",
      "date_modified": "2026-10-30T13:35:00.000Z",
      "tags": [
        "android",
        "kotlin",
        "compose",
        "architecture"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    },
    {
      "id": "https://ardakazanci.com/posts/observing-compose-runtime-with-compositionobserver/",
      "url": "https://ardakazanci.com/posts/observing-compose-runtime-with-compositionobserver/",
      "external_url": "https://ardakazanci.com/posts/observing-compose-runtime-with-compositionobserver.md",
      "title": "Observing Compose Runtime with CompositionObserver",
      "summary": "Track state reads and recomposition scope lifecycles with CompositionObserver, and understand the observation fixes in Compose Runtime 1.13.0-beta01.",
      "content_text": "Compose Runtime associates state reads with recomposition scopes. `CompositionObserver` is an experimental API for observing those relationships. If you're building a recomposition analysis tool, it lets you track which state a scope reads, when it becomes invalid, and when it leaves the composition. [API reference](https://developer.android.com/reference/kotlin/androidx/compose/runtime/tooling/CompositionObserver).\n\nAttach an observer to an existing composition with `composition.setObserver(observer)`:\n\n```kotlin title=\"AttachObserver.kt\"\nimport androidx.compose.runtime.Composition\nimport androidx.compose.runtime.ExperimentalComposeRuntimeApi\nimport androidx.compose.runtime.tooling.CompositionObserver\nimport androidx.compose.runtime.tooling.CompositionObserverHandle\nimport androidx.compose.runtime.tooling.setObserver\n\n@OptIn(ExperimentalComposeRuntimeApi::class)\nfun attachObserver(\n    composition: Composition,\n    observer: CompositionObserver\n): CompositionObserverHandle? = composition.setObserver(observer)\n\n// Retain the returned handle and detach when finished:\n// handle?.dispose()\n```\n\nThe caller supplies an observer implementation and owns the returned handle. A `null` handle means the composition doesn't support observation. The API belongs to `androidx.compose.runtime:runtime` and requires `ExperimentalComposeRuntimeApi` opt-in. [Registration API](https://developer.android.com/reference/kotlin/androidx/compose/runtime/tooling/package-summary#extension-functions).\n\nThe observer receives composition start/end events and scope lifecycle callbacks. `onReadInScope(scope, value)` is particularly useful: it exposes the relationship between a scope and the object it reads. `value` is usually the snapshot state instance, rather than the value stored inside it.\n\nFor example, if a counter reads a `MutableState<Int>`, an analysis tool can associate that state object with the reading scope. When the scope is invalidated, `onScopeInvalidated` reports the associated object, or `null` for direct invalidation through `scope.invalidate()`. State-driven invalidations aren't necessarily reported at the moment of the write. [Callback contracts](https://developer.android.com/reference/kotlin/androidx/compose/runtime/tooling/CompositionObserver).\n\nThere are two limits to keep in mind. An invalidated scope isn't guaranteed to execute again: its parent might remove the branch containing it. Also, `onEndComposition` marks the end of composition processing; it doesn't mean a frame has been drawn. These callbacks shouldn't be treated as a direct measurement of frame time. [Scope lifecycle](https://developer.android.com/reference/kotlin/androidx/compose/runtime/tooling/CompositionObserver).\n\n## What changed in 1.13.0-beta01?\n\nThe observer is now selected once at the beginning of a composition pass and kept for the entire pass. If you call `setObserver` halfway through, the replacement takes effect on the next pass. This keeps a pass's events associated with the same observer.\n\nMovable content insertion now also reports `onBeginComposition` and `onEndComposition`. An observation tool needs those boundaries to interpret its event sequence consistently. Both fixes shipped in [Compose Runtime 1.13.0-beta01](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.13.0-beta01); the API itself predates that release.",
      "image": "https://ardakazanci.com/og/observing-compose-runtime-with-compositionobserver.png",
      "date_published": "2026-10-29T16:55:00.000Z",
      "date_modified": "2026-10-29T16:55:00.000Z",
      "tags": [
        "kotlin",
        "android",
        "compose",
        "debugging"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    },
    {
      "id": "https://ardakazanci.com/posts/kotlin-2-5-full-value-classes/",
      "url": "https://ardakazanci.com/posts/kotlin-2-5-full-value-classes/",
      "external_url": "https://ardakazanci.com/posts/kotlin-2-5-full-value-classes.md",
      "title": "Kotlin 2.5: What full value classes and value objects mean for Android",
      "summary": "Full value classes and value objects in Kotlin 2.5, explained through an Android video selection, data class trade-offs, and performance limits.",
      "content_text": "Kotlin 2.5.0-Beta2 introduces experimental **full value classes**. Previously, inline value classes could hold one stored property. Full value classes allow several properties to describe a value together.\n\nConsider the selected range in an Android video editor:\n\n```kotlin title=\"PlaybackRange.kt\"\n// Kotlin 2.5.0-Beta2 with -Xfull-value-classes\nvalue class PlaybackRange(\n    val startMs: Long,\n    val endMs: Long\n) {\n    init {\n        require(startMs >= 0 && startMs < endMs)\n    }\n}\n\nfun main() {\n    val selected = PlaybackRange(10_000L, 30_000L)\n    val restored = PlaybackRange(10_000L, 30_000L)\n\n    println(selected == restored) // true\n    // selected === restored     // Compilation error\n}\n```\n\nBoth ranges describe the same section of the video. One might come from the editor and the other from saved data.\n\nA data class already provides equality based on its properties. The additional rule here is that a full value class prohibits direct **identity comparisons** on its type. This catches code that compares the ranges by object reference instead of their timestamps.\n\nThere's a trade-off. The compiler generates `equals()`, `hashCode()` and `toString()`, but no automatic `copy()` or `componentN()` functions. Name-based destructuring is available instead. [Kotlin documentation](https://kotlinlang.org/docs/whatsnew-eap.html#full-value-classes).\n\nThat doesn't mean the entire screen state needs to become a value class:\n\n```kotlin title=\"PlayerUiState.kt\"\ndata class PlayerUiState(\n    val selection: PlaybackRange? = null,\n    val isPlaying: Boolean = false\n)\n\nval updated = PlayerUiState().copy(\n    selection = PlaybackRange(10_000L, 30_000L)\n)\n```\n\nThe range can be a full value class while the UI state keeps the convenience of `copy()`. Small value models like this are where I'd first evaluate the feature in an Android project.\n\nFor results that carry no data, there's also **value object**:\n\n```kotlin title=\"PlaybackResult.kt\"\nsealed interface PlaybackResult\n\nvalue class Ready(val range: PlaybackRange) : PlaybackResult\nvalue object NoSelection : PlaybackResult\n```\n\n`Ready` carries the range. `NoSelection` simply indicates that nothing is selected. A data object could support the same application flow. A value object adds the restriction against direct identity comparisons on its type, so existing data objects don't need replacing. [Value objects](https://kotlinlang.org/docs/whatsnew-eap.html#value-objects).\n\nPerformance requires some restraint. The initial implementation does not inline full value classes, and there's no guaranteed reduction in allocations or GC work. Immutability is shallow too: a mutable list stored inside the class remains mutable. [Runtime representation in KEEP-0454](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0454-better-immutability-value-classes-MFVC.md#representation).\n\nAutomatic Compose recomposition improvements aren't promised either. The KEEP discusses possible future compiler and state-tracking optimizations, with their benefits still to be investigated. [Compose considerations](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0454-better-immutability-value-classes-MFVC.md#mfvc-and-compose).\n\nTo try the feature in a module using Kotlin 2.5.0-Beta2, add this to its `build.gradle.kts`:\n\n```kotlin title=\"build.gradle.kts\"\nkotlin {\n    compilerOptions {\n        freeCompilerArgs.add(\"-Xfull-value-classes\")\n    }\n}\n```\n\nI'd consider it for small models such as ranges or coordinates. For a screen state with frequent `copy()` calls, I'd want a concrete benefit before migrating. Existing `@JvmInline` declarations can stay as they are; they remain valid. [Inline migration](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0468-inline-value-classes.md#existing-jvm-code).",
      "image": "https://ardakazanci.com/og/kotlin-2-5-full-value-classes.png",
      "date_published": "2026-10-29T12:00:00.000Z",
      "date_modified": "2026-10-29T12:00:00.000Z",
      "tags": [
        "kotlin",
        "android"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    },
    {
      "id": "https://ardakazanci.com/posts/kotlin-coroutines-debugging-stacktrace-recoverable/",
      "url": "https://ardakazanci.com/posts/kotlin-coroutines-debugging-stacktrace-recoverable/",
      "external_url": "https://ardakazanci.com/posts/kotlin-coroutines-debugging-stacktrace-recoverable.md",
      "title": "Kotlin Coroutines Debugging Recipes: StackTraceRecoverable",
      "summary": "Keep custom exception fields intact when coroutine stack trace recovery copies an exception.",
      "content_text": "import Mermaid from \"@/components/Mermaid.astro\";\n\n## Did you know about StackTraceRecoverable?\n\nAn exception thrown inside `async` tells you where it failed. The harder question is where another coroutine received that failure through `await()`.\n\nKotlin 2.4.20 adds the experimental `StackTraceRecoverable` interface to the standard library. It lets custom exceptions define how they are copied for coroutine stack trace recovery, without depending on `kotlinx.coroutines` in the exception class itself. [Kotlin release notes](https://kotlinlang.org/docs/whatsnew2420.html#support-for-coroutine-stack-trace-recovery).\n\nThis matters when an exception carries extra state, such as an API error code. Override `copyForStackTraceRecovery()` to preserve that state and keep the original exception as the copy's `cause`. The recovery machinery supplies the additional coroutine frames. [API contract](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.coroutines.debug/-stack-trace-recoverable/copy-for-stack-trace-recovery.html).\n\n## Keep the error code\n\n```kotlin title=\"StackTraceRecovery.kt\" {14-15}\nimport kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi\nimport kotlin.coroutines.debug.StackTraceRecoverable\nimport kotlinx.coroutines.*\n\n@OptIn(ExperimentalStdlibCoroutineSupportApi::class)\nclass ApiException private constructor(\n    val errorCode: Int,\n    cause: Throwable?\n) : Exception(\"API error: $errorCode\", cause),\n    StackTraceRecoverable<ApiException> {\n\n    constructor(errorCode: Int) : this(errorCode, null)\n\n    override fun copyForStackTraceRecovery(): ApiException =\n        ApiException(errorCode, this)\n}\n\nfun main() = runBlocking {\n    supervisorScope {\n        // The coroutine that produces the exception.\n        val request = async<Int> {\n            delay(100)\n            throw ApiException(errorCode = 503)\n        }\n\n        try {\n            // The exception is rethrown here to the consumer.\n            request.await()\n        } catch (e: ApiException) {\n            println(e.errorCode) // 503\n            e.printStackTrace()\n        }\n    }\n}\n```\n\n`supervisorScope` keeps the failed `async` child from cancelling its parent before the failure can be handled at `await()`. The copy retains `errorCode = 503`; `this` links it to the original exception.\n\n<Mermaid\n  caption=\"With stack trace recovery enabled, the receiving coroutine gets a recovered copy.\"\n  code={'flowchart LR\\n  A[\"async: throw ApiException\"] --> B[\"Copy + coroutine frames\"]\\n  B --> C[\"await: receive copy\"]'}\n/>\n\n## Enable recovery on the JVM\n\nUse Kotlin 2.4.20 or later with a compatible `kotlinx-coroutines-core` dependency. For this JVM example, add these **VM options** to the run configuration:\n\n```text title=\"VM options\"\n-Dkotlinx.coroutines.debug=on\n-Dkotlinx.coroutines.stacktrace.recovery=true\n```\n\nDebug mode enables recovery by default; the second option makes the intent explicit. The interface is available in common code, but `kotlinx.coroutines` uses it for recovery only on the JVM. This is a JVM demo, not code to run on Android's main thread. [Coroutine debugging guide](https://kotlinlang.org/docs/coroutines-debugging.html).\n\nPrinting `503` alone does not prove recovery ran—the original exception carries that value too. Inspect the recovered stack trace and its cause. Return `null` from `copyForStackTraceRecovery()` if copying should be disabled for that exception type.",
      "image": "https://ardakazanci.com/og/kotlin-coroutines-debugging-stacktrace-recoverable.png",
      "date_published": "2026-10-27T09:00:00.000Z",
      "date_modified": "2026-10-27T09:00:00.000Z",
      "tags": [
        "kotlin",
        "coroutines",
        "debugging"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    },
    {
      "id": "https://ardakazanci.com/posts/merhaba/",
      "url": "https://ardakazanci.com/posts/merhaba/",
      "external_url": "https://ardakazanci.com/posts/merhaba.md",
      "title": "Hello",
      "summary": "The purpose of this blog: learn, simplify, and share.",
      "content_text": "Hi, I'm Arda.\n\nThis is where I share what I learn about software development, clean code, and useful code snippets.\n\nMy goal is to make complex ideas easier to understand through practical examples. My mission is to share what I learn and help others build better software.\n\nShort, clear, and useful. Welcome.",
      "image": "https://ardakazanci.com/og/merhaba.png",
      "date_published": "2026-10-27T06:00:00.000Z",
      "date_modified": "2026-10-27T06:00:00.000Z",
      "tags": [
        "getting-started"
      ],
      "authors": [
        {
          "name": "Arda Kazancı",
          "url": "https://ardakazanci.com/about/"
        }
      ]
    }
  ]
}
