I opened a coroutine to insert data into my database with ROOM, and the insert method was suspend. When I did that alone inside the coroutine, everything worked fine. Then I wanted to get a bitmap and store it locally, and also add that local path to the database object. Sounded like an easy task, so I started building the url connection and it wouldn't work outside a coroutine or asynctask, so I put it inside the same coroutine as when I am inserting the data into the room database, and I had to make the Dispatchers.IO to make it work. The bitmap downloaded and stored in the external storage, but then the data wasn't storing into the room database, it kept giving me an exception "job was cancelled". So I removed the suspend modifier to the room call and it worked...
So my questions are, why didn't it work with a suspend function when I added the other url connection inputstreams? Is it the connections themselves, the Dispatchers.IO, or something else? And with the room insert method not being a suspend method now, is it still run on the IO thread anyway, but as a blocking function, and not on the main UI thread? Thanks for any help
Attached is the coroutine request and DAO
viewModelScope.launch(Dispatchers.IO) {
try {
val inputStream = url.openConnection().getInputStream()
val image = BitmapFactory.decodeStream(inputStream)
val imagesPath = File("$storagePath/RecordImages/")
imagesPath.mkdirs()
val imageFile = File(imagesPath, imageName)
val outputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()
val record = Record(recordPosition, title, artist, imageName)
recordRepository.insert(record)
} catch (exception: Exception) {
exception.localizedMessage?.let { Log.d(SearchResultsAdapter.TAG, it) }
}
}
DAO
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertRecord(word: Record)
CodePudding user response:
by using suspend keyword on a DAO function you're just saying to the room compiler that this work will happen asynchronously so there is no chance to do it on the main thread or without a coroutine that's it.
When you use suspend
keyword the room compiler knows that this function can only be called from a coroutine so it does not matter which dispatcher you're using room will move this operation to Dispacthers.IO
internally.
probably by using withContext(Dispachters.IO)
according to documentation of this method it will throw CancellationExeption
if parent coroutine is cancelled, so I think the coroutine that you launched is cancelled before the insert
is returned.
when you don't use suspend
I think room can't use correct dispatcher/use coroutine features to do that for you since this function may or may not be called from a coroutine as a result withContext(Dispacters.IO)
will not be in the generated code so that is why you don't get the exception.
for more deeper understanding you can watch this
CodePudding user response:
Correct coroutine behavior is that all currently running coroutines are cancelled when the CoroutineScope they are running on is cancelled. viewModelScope
is specifically intended for coroutines that should be canceled when the associated ViewModel is destroyed.
For a function in a coroutine to be cancelled when the coroutine is cancelled, it must cooperate with cancellation. Most well-behaved suspend functions should do this, and indeed, the ones from the Room database library do properly cooperate with cancellation.
When you use a blocking function instead, it is not cooperative with cancellation, so it keeps running even after the coroutine is cancelled. So when you used the blocking function, you accidentally subverted the intended coroutine behavior. This is a not a good solution to the problem of your coroutine being cancelled when you don't want it to. It's fragile, because it would be very easy to put in some other function call that is cooperative with cancellation before your blocking call, and then it would never be reached in the first place. It also communicates a different intent than you want. Calling code in viewModelScope
means that you want it to be cancelled when the ViewModel is destroyed.
The correct solution is to use a CoroutineScope with appropriate lifetime for the action you want to perform.
You could expose a CoroutineScope that you create for your repository or in your Application subclass. Or you could use the pre-existing GlobalScope if you don't mind compiler warnings.
If you create your own in your repository, it's up to you if you ever want to cancel it. If your repository is a singleton, then I don't think it makes sense to ever cancel it. Same goes for if you make one in your Application class.
(By the way, you missed closing your input stream. I fixed that using a use
call, which is the idiomatic way to work with Closeables. You were also failing to close your output stream in the case of an error. That's also fixed with a use
call.)
class MyRepository(val context:Context) {
//...
/** A CoroutineScope that lives as long as the Repository. */
val repositoryScope = MainScope() CoroutineName("MyRepository")
//...
}
// In ViewModel:
recordRepository.launch(Dispatchers.IO) {
try {
val image = url.openConnection().getInputStream().use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}
val imagesPath = File("$storagePath/RecordImages/")
imagesPath.mkdirs()
val imageFile = File(imagesPath, imageName)
FileOutputStream(imageFile).use { outputStream ->
image.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
val record = Record(recordPosition, title, artist, imageName)
recordRepository.insert(record)
} catch (exception: Exception) {
exception.localizedMessage?.let { Log.d(SearchResultsAdapter.TAG, it) }
}
}
For other behaviors, such as if you want to only make the database insert if the image operation you're doing is complete, but then once you start it you want to guarantee it finishes, you could run the last part in the other scope:
// In ViewModel:
viewModelScope.launch(Dispatchers.IO) {
try {
val image = url.openConnection().getInputStream().use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}
val imagesPath = File("$storagePath/RecordImages/")
imagesPath.mkdirs()
val imageFile = File(imagesPath, imageName)
FileOutputStream(imageFile).use { outputStream ->
image.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
val record = Record(recordPosition, title, artist, imageName)
recordRepository.repositoryScope.launch {
recordRepository.insert(record)
}.join() // join call is optional. It's if you want to do something
// back in the ViewModel scope after the insert is complete.
} catch (exception: Exception) {
exception.localizedMessage?.let { Log.d(SearchResultsAdapter.TAG, it) }
}
}