跪拜 Guibai
← Back to the summary

SharingStarted Is the Real Engine Behind Kotlin's stateIn

Many Android developers use stateIn() with code that looks almost exactly like this:

stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = UiState.Loading
)

This has already become the standard way to write it.

In reality:

stateIn does only two things:

1. Upgrades a cold flow to a hot flow
2. Controls the upstream lifecycle via SharingStarted

The most critical part here is actually

SharingStarted

It determines:

In a sense:

SharingStarted is the lifecycle manager in the world of Flow.


1. The Essence of stateIn

Suppose we have a Repository like this:

fun userFlow(): Flow<User> = flow {
    println("Starting to request user info")
    emit(api.getUser())
}

This is a standard cold flow.

This means:

One collect
One execution

This situation often occurs in projects:

The avatar at the top of the page needs user info:

The user card in the middle of the page also needs user info:

At this point:

Avatar collects once
Profile collects once

Log:

Starting to request user info
Starting to request user info

Because:

Flow is cold by default

Every collect
re-executes the entire upstream

So:

val user = repository.userFlow()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(),
        initialValue = User.Empty
    )

Now:

Multiple Collectors
share the same upstream

Log:

Starting to request user info

Regardless of:

They all share the same upstream execution result.


In fact, in the Room scenario:

val messages = dao.observeMessages()

There might simultaneously be:

Without stateIn():

Room registers 3 Observers
SQLite executes 3 queries

After using stateIn():

Room listens only once
Multiple consumers share the result

This is actually the core value of stateIn():

Sharing expensive upstream operations with multiple consumers.


2. The Real Core: SharingStarted

The official API provides three strategies:

Strategy Start Trigger Stop Trigger Use Case
Eagerly Starts immediately When Scope is destroyed Global data
Lazily On first subscription When Scope is destroyed Lazy cache
WhileSubscribed When subscribed After unsubscription timeout UI data

3. Eagerly: Starts Working Immediately on Project Launch

val config = repository.configFlow()
    .stateIn(
        viewModelScope,
        SharingStarted.Eagerly,
        Config.Empty
    )

Characteristics:

ViewModel created
Upstream starts immediately

Even if:

There are no Collectors

The upstream continues to run.

For example:

Application starts
↓
ViewModel created
↓
Network request begins
↓
The page hasn't even opened yet

Suitable for:

In essence:

Data is more important than UI

4. Lazily: Starts on First Use

val userInfo = repository.userFlow()
    .stateIn(
        viewModelScope,
        SharingStarted.Lazily,
        User.Empty
    )

Characteristics:

No one subscribes
Doesn't work

Someone subscribes
Starts working

Subscription ends
Continues working

It only starts once.

After that, it never stops.

Lifecycle:

First collect
    ↓
Upstream starts
    ↓
Page exits
    ↓
Continues running
    ↓
ViewModel destroyed
    ↓
Ends

Suitable for:

For example:


5. WhileSubscribed: The Optimal Solution for UI Scenarios

This is the officially recommended approach.

val uiState = repository.dataFlow()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(),
        initialValue = UiState.Loading
    )

Behavior:

A page is observing
    ↓
Upstream starts

Page exits
    ↓
Upstream stops

It aligns very well with:

UI lifecycle

Therefore:

Most ViewModels should prioritize WhileSubscribed.


6. Two Easily Overlooked Parameters

Many people have used this for years:

SharingStarted.WhileSubscribed(5000)

But don't know what these two parameters actually do.

Full definition:

WhileSubscribed(
    stopTimeoutMillis,
    replayExpirationMillis
)

1. stopTimeoutMillis

For example:

SharingStarted.WhileSubscribed(
    stopTimeoutMillis = 5000
)

Meaning:

After the last subscriber disappears
continue to live for 5 seconds

Lifecycle:

Activity A collects
    ↓
User rotates screen
    ↓
Old Activity destroyed
    ↓
No subscribers
    ↓
Wait 5 seconds
    ↓
New Activity established
    ↓
Re-subscribes

Because:

Re-subscription happens within 5 seconds

So the upstream doesn't stop.

This avoids:

This is essentially a:

Debounce mechanism

If you set:

WhileSubscribed(0)

Then:

Page exits
Stops immediately
Page rebuilt
Restarts

Rotating the screen once:

Start
Stop
Start
Stop
Start

The logs can be quite dramatic.


2. replayExpirationMillis

For example:

WhileSubscribed(
    stopTimeoutMillis = 5000,
    replayExpirationMillis = 0
)

It controls:

When the cache expires

Default value:

Long.MAX_VALUE

Meaning:

The cache never expires

Suppose:

val flow = flow {
    delay(2000)
    emit(Random.nextInt())
}

First time entering the page:

Loading
↓
42

Exit the page.

Re-enter:

42
↓
Re-request
↓
88

Because:

42 was cached

So the user sees the old value first.

If:

WhileSubscribed(
    stopTimeoutMillis = 5000,
    replayExpirationMillis = 0
)

The behavior becomes:

Page exits
↓
Cache cleared immediately
↓
Re-enter page
↓
Shows initialValue
↓
Re-request
↓
88

Log:

0
↓
88

The old value will no longer appear.


7. When Should You Set replayExpirationMillis = 0?

Suitable for:

For this data:

Old values are meaningless

Showing users old data is actually an error.

And for these scenarios:

Caching is actually a good thing.

Because:

Users don't want to see splash screens and Loading indicators

8. Recommended Configuration

For the vast majority of ViewModels:

stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(
        stopTimeoutMillis = 5000
    ),
    initialValue = UiState.Loading
)

For real-time data:

stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(
        stopTimeoutMillis = 5000,
        replayExpirationMillis = 0
    ),
    initialValue = UiState.Empty
)

For global state:

stateIn(
    scope = applicationScope,
    started = SharingStarted.Eagerly,
    initialValue = InitialState
)