Description
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