Skip to content

getStringOrNullFlow() may cause ANR on main thread despite immediate value expected #233

Open
@AseevEIDev

Description

@AseevEIDev

Hi, I'm encountering unexpected behaviour with getStringOrNullFlow() that can lead to an ANR (Application Not Responding) when calling firstOrNull() from the main thread.

In our case, we use SharedPreferencesSettings to wrap Android's SharedPreferences, created like this:

SharedPreferencesSettings(
    context.getSharedPreferences(name, Context.MODE_PRIVATE),
    commit = true
)

We expect getStringOrNullFlow() to emit the current cached value immediately, which is important for certain use cases — especially when we want to synchronously grab a value on the main thread to skip loading states (e.g., showing a UI screen directly if a value already exists).

However, calling:

settings.getStringOrNullFlow("some_key").firstOrNull()

on the main thread can hang and eventually trigger an ANR, despite the fact that the flow should emit immediately via callbackFlow.

Crashlytics stacktrace:

main (runnable):tid=1 systid=16833 
       at kotlin.coroutines.CombinedContext.get(CoroutineContextImpl.kt:120)
       at kotlinx.coroutines.AbstractCoroutine.<init>(AbstractCoroutine.kt:50)
       at kotlinx.coroutines.internal.ScopeCoroutine.<init>(Scopes.kt:14)
       at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:285)
       at kotlinx.coroutines.flow.internal.ChannelFlow.collect$suspendImpl(ChannelFlow.kt:118)
       at kotlinx.coroutines.flow.internal.ChannelFlow.collect(ChannelFlow.kt:6)
       at kotlinx.coroutines.flow.DistinctFlowImpl.collect(Distinct.kt:68)
       at kotlinx.coroutines.flow.FlowKt__ReduceKt.firstOrNull(FlowKt__Reduce.kt:230)
       at kotlinx.coroutines.flow.FlowKt.firstOrNull(Flow.kt:1)
       at com.token.data.TokenRepositoryImpl.getSelectedToken(TokenRepositoryImpl.kt:64)
       at com.token.data.TokenRepository$DefaultImpls.getSelectedToken$default(TokenRepository.java:21)
       at com.transfer.domain.TransferChannelInteractorImpl.loadChannelWithDetailsForCountry(TransferChannelInteractorImpl.kt:42)
       at com.transfer.create.TopupCreateViewModel.getChannel(TopupCreateViewModel.kt:148)
       at com.transfer.create.TopupCreateViewModel.access$getChannel(TopupCreateViewModel.kt:38)
       at com.transfer.create.TopupCreateViewModel$observeQuote$1$invokeSuspend$$inlined$flatMapLatest$1.invokeSuspend(Merge.kt:196)
       at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
       at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:232)
       at android.os.Handler.handleCallback(Handler.java:991)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loopOnce(Looper.java:232)
       at android.os.Looper.loop(Looper.java:317)
       at android.app.ActivityThread.main(ActivityThread.java:8934)
       at java.lang.reflect.Method.invoke(Native method)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)

Of course we can use settings.getStringOrNull("some_key") and it solves an issue, but it's not always convenient.

⚙️ Versions used
Kotlin: 2.1.10
Coroutines: 1.10.2
Settings: 1.3.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions