Hi Everyone. I had a question. For shared KMP serv...
# multiplatform
s
Hi Everyone. I had a question. For shared KMP services/managers that perform real I/O like DataStore, DB, files, or network, what should our team standard be for dispatcher ownership? Options to align on 1. Caller-owned threading Low-level managers/services stay dumb, and every caller must invoke them from the correct dispatcher (Dispatchers.IO, etc.). 2. Callee-owned threading Any low-level class that performs I/O must enforce safe execution internally, usually via injected dispatchers. 3. Mixed rule by layer Pure repositories/services can stay dumb, but storage/persistence boundaries like DataStore/DB/file access must enforce their own dispatcher internally. Follow-up questions - Should this standard be the same for Android and iOS/KMP interop paths? - Do we want convention-based safety or contract-based safety for disk/database I/O? - If dispatchers are enforced internally, should we inject them for testability? - Should Swift/KMP bridges stay thin, or can they own platform-specific dispatcher fixes?
s
For 1) I disagree. The manager/service itself knows how their methods are implemented and that determines which dispatchers are needed. If a suspend fun of such a manager/service Does blocking calls (eg IO, locks, plain Mutex, etc), it wraps its implementation in Dispatchers.IO Does a long running calculation, wraps the implementation in Dispatchers.Default Etc This should not be a concern of the caller.
For 2), I agree. Inject dispatchers but for testing purposes only. Related to the first bullet point ๐Ÿ˜
Tldr: callee uses their own appropriate dispatcher. Callers know nothing about which dispatcher is being used by the implementation and trust it to be callable on main thread. And inject dispatchers for tests.
c
I think a good rule of thumb is, any function that performs some heavy operation (network/db/etc) should internally define the dispatcher it's invoked on; no it's callers. Callers of that function should not need to worry or know about that. Also worth noting that most libraries are already handling this - retrofit, ktor, room etc all execute on background threads. So dispatcher switching is not explicitly needed in your own code when using these libraries
s
Thanks All, I guess I need a refactor!! My code is coroutine safe and performant on prod, but the design, its easy to misuse if someone failed to be certain. mostly my viewmodels used to be
fun getData() {
viewModelScope.launch(){
val result = withContext(<http://Dispatchers.IO|Dispatchers.IO>){
repo.getData()
}
result.fold(_...)
}
c
The problem here is that
repo.getData
could be invoked in the wrong thread. That isn't a concern of the caller. You'll also have boiler plate as every class that calls this function will have to wrap it in a `withContext`block
s
Thanks,
u
In my case, I wrap operations at the use case level and let the use case own the dispatcher. Threading is an execution concern, not a data-access concern. This reduces unnecessary dispatcher code inside repositories and keeps them simpler. It also improves testability, since dispatcher policy is explicit at the use case layer. As long as we follow the convention that repositories are accessed through use cases, I donโ€™t see it causing issues. There can still be exceptions for clear blocking boundaries (e.g., file I/O), where enforcing a dispatcher inside the repository makes sense.
s
Hey @์ •์„ธํ˜„ so do we create use case for every action?
u
Yes, I usually wrap repository calls with a use case, even for simple actions.