I work in multiple code bases, some really really large and some that are small/simple.
The architectures are pretty close, mainly MVVM, some with a flavor of MVI. But what is consistent across all these codebases in terms of threading, is that all methods within the view (activity/fragment) and ViewModel are main-safe. That is, you can call all methods safely on main thread and won't block the UI.
Using the repository/useCase pattern, public methods contained in this classes should also be main safe. It's recommended to pass the different dispatchers into your repo/useCase classes so you can easily test as well. But the main thing to point out here is that all public methods are main-safe.
Thread switching (using
Dispatchers.IO, Dispatchers.Default etc) is contained within the repository/use case classes. Whether it be calling some remote data source, db, or data processing etc. Coroutines and some helpful extensions like viewModelScope make this all clean and easy to achieve. So an example would be a suspend method loadUsers in the repo, which is main-safe, since it internally uses withContext to switch to
Dispatchers.IO for loading users.
I'm probably giving an over simplification here but I think you get the picture I'm trying to paint 😊.
Idk if this exactly answers all or some of your questions.. 2a.m. here and I'm struggling to fall asleep haha but my brain is a bit fried atm. Just came across this and thought I'd share what I typically do.
Edit: just want to clarify that what I explained above is more a general pattern. It's acceptable to use Dispatchers.Default for example, even in your ViewModel. I think the most important rule is more that any public method is main-safe. And if you follow the architecture j described above, the view will be as dumb/simple as possible, so there is no need to handle thread switching in the view layer. The ViewModel contains straightforward business logic, such as retrieving data from repo layer and managing/updating a state, so there's really no need to handle thread switching here as well. Which then brings the last part which is your data/repo layer. Which basically exposes public main-safe APIs, but internally that is where thread switching/if any, is typically happening. Which is typically the case, fetching data from web service, local db, mapping Backend models etc. Hope that clears up any additional confusion.