Issue
My use case is as follows:
Imagine that there is an Android Fragment that allows users to search for Grocery items in a store. There's a Search View, and as they type, new queries are sent to the Grocery item network service to ask for which items match the query. When successful, the query returns a list of Grocery items that includes the name, price, and nutritional information about the product.
Locally on the Android device, there is a list of known for "items for sale" stored in a raw file. It's in the raw
resources directory and is simply a list of grocery item names and nothing else.
The behavior we wish to achieve is that as the user searches for items, they are presented with a list of items matching their query and a visual badge on the items that are "For Sale"
The constraints I am trying to satisfy are the following:
When the user loads the Android Fragment, I want to parse the raw text file asynchronously using a Kotlin coroutine using the IO Dispatcher. Once parsed, the items are inserted into the Room database table for "For Sale Items" which is just a list of names where the name is the primary key. This list could be empty, it could be large (i.e. >10,0000).
Parallel, and independent of #1, as the user types and makes different queries, I want to be sending out network requests to the server to retrieve the Grocery Items that match their query. When the query comes back successfully, these items are inserted into a different table in the Room database for Grocery Items
Finally, I only want to render the list returned from #2 once I know that the text file from #1 has been successfully parsed. Once I know that #1 has been successfully parsed I want to join the tables in the database on name and give that LiveData to my ViewModel to render the list. If either #1 or #2 fail, I want the user to be given an "Error occurred, Retry" button
Where I am struggling right now:
Seems achievable by simply kicking off a coroutine in ViewModel init that uses the IO Dispatcher. This way I only attempt to parse the file once per ViewModel creation (I'm okay with reparsing it if the user kills and reopens the app)
Seems achievable by using another IO Dispatcher coroutine + Retrofit + Room.
Satisfying the "Only give data to ViewModel when both #1 and #2 are complete else show error button" is the tricky part here. How do I expose a LiveData/Flow/something else? from my Repository that satisfies these constraints?
Solution
When you launch coroutines, they return a Job object that you can wait for in another coroutine. So you can launch a Job for 1, and 3 can await it before starting its flow that joins tables.
When working with Retrofit and Room, you can define your Room and Retrofit DAOs/interfaces with suspend
functions. This causes them to generate implementations that internally use an appropriate thread and suspend (don't return) until the work of inserting/updating/fetching is complete. This means you know that when your coroutine is finished, the data has been written to the database. It also means it doesn't matter which dispatcher you use for 2, because you won't be calling any blocking functions.
For 1, if parsing is a heavy operation, Dispatchers.Default is more appropriate than Dispatchers.IO, because the work will truly be tying up a CPU core.
If you want to be able to see if the Job from 1 had an error, then you actually need to use async
instead of launch
so any thrown exception is rethrown when you wait for it in a coroutine.
3 can be a Flow from Room (so you'd define the query with the join in your DAO), but you can wrap it in a flow
builder that awaits 1. It can return a Result, which contains data or an error, so the UI can show an error state.
2 can operate independently, simply writing to the Room database by having user input call a ViewModel function to do that. The repository flow used by 3 will automatically pick up changes when the database changes.
Here's an example of ViewModel code to achieve this task.
private val parsedTextJob = viewModelScope.async(Dispatchers.Default) {
// read file, parse it and write to a database table
}
val theRenderableList: SharedFlow<Result<List<SomeDataType>>> = flow {
try {
parsedTextJob.await()
} catch (e: Exception) {
emit(Result.failure(e)
return@flow
}
emitAll(
repository.getTheJoinedTableFlowFromDao()
.map { Result.success(it) }
)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun onNewUserInput(someTextFromUser: String) {
viewModelScope.launch {
// Do query from Retrofit.
// Parse results and write to database.
}
}
If you prefer LiveData to SharedFlow, you can replace theRenderableList
above with:
val theRenderableList: LiveData<Result<List<SomeDataType>>> = liveData {
try {
parsedTextJob.await()
} catch (e: Exception) {
emit(Result.failure(e)
return@liveData
}
emitSource(
repository.getTheJoinedTableFlowFromDao()
.map { Result.success(it) }
.asLiveData()
)
}
Answered By - Tenfour04
Answer Checked By - Senaida (JavaFixing Volunteer)