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:
- When the upstream starts
- When the upstream stops
- When the cache expires
- Whether resources are released after leaving a page
- Whether data is re-fetched when a page is rebuilt
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:
- 1 Collector
- 2 Collectors
- 10 Collectors
They all share the same upstream execution result.
In fact, in the Room scenario:
val messages = dao.observeMessages()
There might simultaneously be:
- Chat list collect
- Unread badge collect
- Message statistics collect
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:
- Login state
- User info
- Configuration center
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:
- Large cached data
- Heavy initialization data
- Data sources you don't want to re-initialize
For example:
- Map SDK
- Player state
- Large configuration data
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:
- Network re-requests
- Database re-listening
- Flow restarts
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:
- Real-time stocks
- Real-time prices
- Order status
- Device status
- Online user count
- Countdowns
For this data:
Old values are meaningless
Showing users old data is actually an error.
And for these scenarios:
- User profile
- Configuration center
- Theme mode
- Settings items
- Login info
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
)