Skip to content

Demand-driven programming

Demand-driven means that expensive resources are automatically (on-demand) only allocated as long as they’re needed and then freed when not needed. This also includes computations which should only happen when they’re needed.

For example, by lazy is not demand-driven because it only allocates resources on first use, but never deallocates.

However, stateIn together with WhileSubscribed allows creating a demand-driven StateFlow that might e.g. communicate with a backend via a WebSocket (let’s say a news ticker).

Normally you want to only keep the connection open as long as there is some UI that displays the data. Once the user logs out or switches to some other screen or just locks his screen you want to close the connection.

The point of being demand-driven is to make this automatic, so your code becomes 100% robust and simple, no matter in which way you change your UI. You should never have to plaster your code with explicit open() and close() logic everywhere because in practice this “imperative” logic leads to bugs. Imagine the a new requirement where you want to show the news widget in a few more screens, so now the connection needs to be kept open across some screens, but on others it can be closed.

The way to model this problem is that some screens “demand” the news ticker. In other words, they have a dependency on the news ticker.

StateFlow and SharedFlow

The coroutines library only provides stateIn and shareIn functions which take a CoroutineScope. This can be quite annoying if you want to e.g. create a function that returns a StateFlow because stateIn keeps a coroutine running in the background as long as the provided CoroutineScope exists. Also, very often in reusable/library code you don’t want to pass scopes around through your whole codebase because that’s just ugly and feels unnecessary. Moreover, you might not even have a proper CoroutineScope available because your computed StateFlow shouldn’t bind to a single ViewModel’s scope, but rather be shared maybe even by multiple ViewModels, but still not consume resources when nobody is using that StateFlow. Ideally you wouldn’t have to deal with CoroutineScope.

This library provides stateOnDemand and shareOnDemand and sharedFlow which only launch an internal coroutine while someone is subscribed, but can safely get garbage collected when nobody is subscribed, anymore.

For example, if you want to create a getIntFlow() function for a key-value store (e.g. SharedPreferences or NSUserDefaults) you might want to return the current result but also allow observing/collecting to receive updates. With stateOnDemand you can create such a result and using toMutable you can make it mutable:

suspend fun KeyValueStore.getIntFlow(key: String, default: Int): MutableStateFlow<Int> =
    callbackFlow {
        // With callbackFlow we define the collect() behavior.
        // In this example, let's use a pseudo-API for getting notified whenever a key gets updated:
        val listener = KeyValueStore.OnChangeListener { changedKey: String ->
            if (changedKey == key) {
                send(getInt(key))
            }
        }
        addListener(listener)
        awaitClose { removeListener(listener) }
    }.stateOnDemand { previous: Wrapped<Int>? ->
        // stateOnDemand takes a getter function which defines the StateFlow.value behavior when nobody collects.
        // The previous value is also passed here, wrapped in a Wrapped() instance (which can be null if this is the
        // first value access). This can be useful for caching.
        getInt(key, default)
    }.toMutable { value: Int ->
        // toMutable defines the StateFlow.value = ... setter behavior
        putInt(key, value)
    }

Here we’ve turned a simple Flow (via callbackFlow) into a StateFlow that is safe for returning from a function and we didn’t need any CoroutineScope. Then we’ve used toMutable to turn that into a MutableStateFlow.

News ticker example

Imagine you want to show the breaking news only, but the backend only provides a list of all latest news. We have to filter the breaking news from that list, but all of that should only happen while the screen is visible. If the user locks the screen or switches the app the connection should stop.

class NewsTicker(scope: CoroutineScope) {
    val latestNews: StateFlow<List<News>> = channelFlow {
        // connect to backend, watch for changes and send() latest news,
        // and close the connection when the flow is cancelled
    }.stateIn(initial = emptyList(), started = WhileSubscribed())
}

// A demand-driven singleton which you can put into your DI
val newsTicker = WhileUsed {
    // it.scope is a MainScope that exists only as long someone depends on newsTicker
    // and is cancelled once it's not needed anymore.
    NewsTicker(it.scope)
}

val breakingNews: StateFlow<List<News>> =
    derived(initial = emptyList(), started = WhileSubscribed()) {
        get(get(newsTicker).latestNews)
            .filter { it.isBreakingNews }
    }

class NewsScreen : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // autoRun only collects in >= STARTED state.
        // When the user e.g. locks the screen or switches to some other app,
        // it cancels collecting because the state is < STARTED.
        autoRun {
            updateScreen(get(breakingNews))
        }
    }

    fun updateScreen(news: List<News>) {
        // ...
    }
}

As you can see, some of this depends on Kotlin’s WhileSubscribed().

WhileUsed allows you to create an on-demand computed singleton that gets disposed as soon as nobody is using it, anymore. Here we’re applying it to only create the NewsTicker object as long as it’s needed.

Internally, WhileUsed is tracking the number of reference, so once that “reference count” goes back to count == 0 it can destroy its value and CoroutineScope.

As an alternative to the autoRun/derived based tracking, you can also use a CoroutineScope to track the dependency lifetime:

// This holds the reference until coroutineScope is cancelled.
// Note: it's lowercase newsTicker, so we're calling the WhileUsed object here.
val newsTickerInstance = newsTicker(coroutineScope)

If even this doesn’t work for you, there are other alternatives like manual reference count tracking or passing a DisposableGroup.