Kotlin: Storing and calling suspend function throws StackOverflow exception - kotlin

I'm trying to implement "Try again" functionality, which means, when some request failed, user will be able to tap on "Try Again" button to resend the same request again.
In short, I have BaseViewModel with
lateinit var pendingMethod: suspend () -> Unit
and
fun runAsync(tryFunction: suspend () -> Unit) {
viewModelScope.launch(errorHandler) {
try {
tryFunction()
} catch (ex: Exception) {
pendingMethod = tryFunction
}
}
}
And from view, when "Try Again" button is clicked, I call
viewModel.runAsync { viewModel.pendingMethod() }
First tap works well, but when I tap second time, it throws
StackOverflow error: stack size 8MB
and bunch of invokeSuspend(..) in the logs, which looks like there are suspend functions call each other infinitely.
Any thoughts about this?
Update:
I have fixed this by storing suspend function in extra variable like this
val temp = viewModel.pendingMethod
viewModel.runAsync { temp() }
Instead of
viewModel.runAsync { viewModel.pendingMethod() }

Your issue can be traced by following points
pendingMethod property is not initialized
tryFunction() call throws kotlin.UninitializedPropertyAccessException
Exception is caught and pendingMethod = tryFunction() which effectivelly means pendingMethod = pendingMethod() // this is recursive call
next time when you call runAsync it essentially calls pendingMethod() but this time its initialized, and since pendingMethod does nothing but call itself, you get the stack overflow error
So how to implement the try again functionality
Lets say you have a suspending function which executes the given request, it can be network call(Retrofit supports suspend keyword) or it can be database access (Room also supports suspend keyword). so your function looks something like
suspend fun executeRequest(request: RequestModel): Result
Now update your ViewModel as
// declare a live data property to notfify user that request has failed
val requestFailed: MutableLiveData<Boolean> = MutableLiveData(false)
// update runAsync
fun runAsync(request: RequestModel) = viewModelScope.launch {
try {
executeRequest(request)
}
catch (ex: Exception) {
requestFailed.postValue(true)
}
}
And in your Activity, Fragment observe the LiveData object to display the error to user
viewModel.requestFailed.observe(this, Observer{
if(it) {
// Show error toast
}
}

Related

Why method not return anything after dao method?

My code stop working after call dao method, if I use GlobalScope code working, but then LiveData not update changes
override suspend fun addNewTask(title: String,priority : Int,date: Date): Boolean {
var isSuccess = false
val newTask = hashMapOf(
"title" to title,
"priority" to priority,
"task_points" to converterListOfListToJson(listOf()),
"date" to Timestamp(date)
)
val user: User? = userDao.getCurrentUser()
val newTaskList = user?.tasks?.toMutableList()
val generatedDoc = db.collection(CollectionNames.tasks).document()
val userTasks = db.collection(CollectionNames.users).document(user!!.id)
generatedDoc.set(newTask)
.addOnSuccessListener { isSuccess = true }
.addOnFailureListener { Log.e(TAG, "Error writing document") }.await()
userTasks.update(
"tasks", FieldValue.arrayUnion(generatedDoc.id)
).await()
newTaskList?.add(generatedDoc.id)
taskDao.insert(Task(generatedDoc.id, title, date, listOf(), priority.toLong())) //after code out
user.tasks = newTaskList!!.toList()
userDao.updateUser(user)
return isSuccess
}
This call of this method, addNewTask full complete but after code just end method
fun createTask(title : String, priority : String,date: Date) {
viewModelScope.launch{
tasksUseCase.addNewTask(title, priority, date)
tasksUseCase.updateTaskFromLocalDB().collect { //it is not called
_taskList.postValue(it)
}
}
}
In DAO I just use annotation update and insert
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(task: Task)
My function call in dialog fragment on click, but with viewModel of parent fragment, function deadlock without errors
.addOnSuccessListener { isSuccess = true } will change the variable to true some time in the future, when the background work is done. Your code that uses isSuccess is called before it has a chance to finish the background work.
When you used GlobalScope, it probably used the default dispatcher Dispatchers.Default, so you had a race condition and in some cases it could succeed.
When you used viewModelScope, it uses Dispatchers.Main, so there is no possibility of the result listener being called before the end of this suspend function unless there is some other suspending call in between.
What you need to do to fix it is run your task in a synchronous, suspending way instead of an asynchronous way. Since I don't know what class generatedDoc is, I can't help with that. Many libraries include extension suspend function, usually named await(), that let you get the result synchronously. If they don't provide that, you can write your own using suspendCancellableCoroutine. There are many other questions about that on here that you can search for to see how to use it.
The problem was that the viewModel was a DialogFragment which was destroyed after the method was called, due to which the ViewModelScope also ceased to exist and the method crashed in the middle

Wear OS Tiles and Media Service

The Wear OS tiles example is great, not so much of an issue but how would one start the background media service that play the songs selected in the primary app, when every I try to start the service, I get the following error. The is no UI thread to reference and the documentation only has to methods for onclick, LoadAction and LaunchAction.
override fun onTileRequest(request: TileRequest) = serviceScope.future {
when(request.state!!.lastClickableId){
"play"-> playClicked()
}....
suspend fun playClicked(){
try {
// Convert the asynchronous callback to a suspending coroutine
suspendCancellableCoroutine<Unit> { cont ->
mMediaBrowserCompat = MediaBrowserCompat(
applicationContext, ComponentName(applicationContext, MusicService::class.java),
mMediaBrowserCompatConnectionCallback, null
)
mMediaBrowserCompat!!.connect()
}
}catch (e:Exception){
e.printStackTrace()
} finally {
mMediaBrowserCompat!!.disconnect()
}
}
ERROR
java.lang.RuntimeException: Can't create handler inside thread Thread[DefaultDispatcher-worker-1,5,main] that has not called Looper.prepare()
serviceScope is running on Dispatchers.IO, you should use withContext(Dispatchers.Main) when making any calls to MediaBrowserCompat.
Responding to the answer above, the serviceScope.future creates a CoroutineScope that will cause the future returned to the service to wait for all child jobs to complete.
If you want to have it run detached from the onTileRequest call, you can run the following, which will launch a new job inside the application GlobalScope and let the onTileRequest return immediately.
"play" -> GlobalScope.launch {
}
The benefit to this is that you don't throw a third concurrency model into the mix, ListenableFutures, Coroutines, and now Handler. LF and Coroutines are meant to avoid you having to resort to a third concurrency option.
Thanks Yuri that worked but, it ended up blocking the UI thread, the solution that is work is below
fun playClicked(){
mainHandler.post(playSong)
}
private val playSong: Runnable = object : Runnable {
#RequiresApi(Build.VERSION_CODES.N)
override fun run() {
mMediaBrowserCompat = MediaBrowserCompat(
applicationContext, ComponentName(applicationContext, MusicaWearService::class.java),
mMediaBrowserCompatConnectionCallback, null
)
mMediaBrowserCompat!!.connect()
}
}
Cool Yuri, the below worked and I think is more efficient
fun playClicked() = GlobalScope.launch(Dispatchers.Main) {
mMediaBrowserCompat = MediaBrowserCompat(
applicationContext, ComponentName(applicationContext, MusicaWearService::class.java),
mMediaBrowserCompatConnectionCallback, null
)
mMediaBrowserCompat!!.connect()
}

CoroutineScope cancel listener

I'm performing some work in a class that is using a Scope:
class MyClass(val scope: CoroutineScope) {
private val state: StateFlow<Int> = someFlow()
.shareIn(scope, started = SharingStared.Eagerly, initialValue = 0)
fun save() {
scope.launch {
save(state.value)
}
}
}
Now I want to clean up when the scope is cancelled. What is the best way to do this? I could come up with this, but that doesn't really sound stable.
init {
scope.launch {
try { delay(10000000000000) }
finally { withContext(Noncancellable) { save(state.value) } }
}
}
Edit: I've modified my snippet to more reflect what I'm doing. The state Flow updates several times per second, and when I invoke the save() method I want to save the state to disk (So I don't want to do this every time the state changes).
Next to that, I want to save the state when the scope is cancelled (i.e. at the very end). This is where I'm having trouble.
There is no such "onCancellation" mechanism on CoroutineScope to my knowledge.
In general, clean up can be "prepared" on the spot when executing the code that requires cleanup. For instance, using an input stream with use { ... } or closing resources with finally blocks.
This will be automatically honored on cancellation (or any other failures, btw), because cancellation of the scope simply generates CancellationExceptions inside running coroutines.
Now, sometimes (as in your case) you have more complex needs, and in that case I would say that the cancellation of the scope is just one thing that happens at the end of some kind of lifecycle, and you can do the cleanup you need at the same place where you cancel the scope.
If you really want to use a workaround like your current parallel coroutine, you can use awaitCancellation instead of a huge delay:
init {
scope.launch {
try { awaitCancellation() }
finally { withContext(Noncancellable) { save(state.value) } }
}
}
But I still don't find it very appealing tbh.
You can use a Exception handler
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("CoroutineExceptionHandler Error", exception.message!!)
stopSelf(startId)
}
Then you can use this Handler as
scope.launch(handler){
// do stuff
}
handler will be called only if an exception is thrown

Kotlin not getting called from view model

I am trying call
override suspend fun getLoginResponse(loginRequest: LoginRequest) = flow {
emit(ApiResult.Loading)
networkCall {
loginService.postLoginResponse(loginRequest)
}.let { apiResult->
apiResult.isSuccessAndNotNull().letOnTrueOnSuspend {
(apiResult.getResult() as? LoginResponse)?.let {
emit(ApiResult.Success(it))
Timber.d(it.toString())
} ?: run { emit(ApiResult.Error(TypeCastException("unknown error.")))
Timber.d(TypeCastException("unknown error."))}
}
}
}.flowOn(Dispatchers.IO)
from my viewModel like this :
private fun loginResponse(email: String, password: String, device: String){
viewModelScope.launch {
try {
var loginRequest = LoginRequest(email, password, device)
loginResponseFromServer = loginRepository.getLoginResponse(loginRequest)
.asLiveData(viewModelScope.coroutineContext+Dispatchers.Default)
Timber.d(loginResponseFromServer.toString())
}
catch (e: NetworkErrorException){
validationError.value = "Network communication error!"
}
}
}
When I debug or run the code getLoginResponse not even calling. Is there anything I am missing?
First of all, getLoginResponse doesn't need to be a suspend function since it just returns a cold Flow. If you remove the suspend modifier, you won't need a coroutine to call it or convert it to LiveData.
Second, a LiveData that is built with .asLiveData() doesn't begin to collect the Flow (remains cold) until it first becomes active. This is in the docs for the function. It becomes active when it receives its first observer, but your code has not begun to observe it, which is why the code in your flow block is never called.
You also don't need to specify a different dispatcher for your LiveData. It doesn't matter which dispatcher you're collecting in since collecting it isn't blocking code.
However, LiveData isn't something that should be collected within a ViewModel. It's for UI to interact. The LiveData should be observed from the Fragment.
You need to move your catching of the network exception into your flow builder. The exception will not be thrown at the time of creating the Flow or LiveData, but rather at the time the request is being made (in the Flow's execution).
I'm not sure exactly how to rewrite your flow builder to properly catch because it has functions I haven't seen. Just a tip, but chaining together lots of scope functions into one statement makes code hard to read and reason about.
So to do this as LiveData, you can change your code as follows:
private fun loginResponse(email: String, password: String, device: String): LiveData<LoginResponse> {
val loginRequest = LoginRequest(email, password, device)
return loginRepository.getLoginResponse(loginRequest)
.asLiveData()
}
And then observe it in your Fragment.
However
LiveData and Flow don't really fit this use case, because you want to make a single request and get a single response. Your repository should just expose a suspend function that returns the response. Then your ViewModel can have a suspend function that just passes through the response by calling the repository's suspend function.

Receiving ResponseAlreadySentException after attempting to respond to ApplicationCall objects passed through StateFlow

I am testing an event driven architecture in KTOR. My Core logic is held in a class that reacts to different Event types being emitted by a StateFlow. EventGenerators push Events into the StateFlow which are picked up by the Core.
However, when the Core attempts to respond to an ApplicationCall embedded in one of my Events I receive an ResponseAlreadySentException and I'm not sure why this would be the case. This does not happen if I bypass the StateFlow and call the Core class directly from the EventGenerator. I am not responding to ApplicationCalls anywhere else in my code, and have checked with breakpoints that the only .respond line is not being hit multiple times.
MyStateFlow class:
class MyStateFlow {
val state: StateFlow<CoreEvent>
get() = _state
private val _state = MutableStateFlow<CoreEvent>(CoreEvent.NothingEvent)
suspend fun update(event: CoreEvent) {
_state.value = event
}
}
My Core class:
class Core(
myStateFlow: MyStateFlow,
coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.IO
) {
init {
CoroutineScope(coroutineContext).launch {
myStateFlow.state.collect {
onEvent(it)
}
}
}
suspend fun onEvent(event: CoreEvent) {
when(event) {
is FooEvent {
event.call.respond(HttpStatusCode.OK, "bar")
}
...
}
}
}
One of my EventGenerators is a Route in my KTOR Application class:
get("/foo") {
myStateFlow.update(CoreEvent.FooEvent(call))
}
However, hitting /f00 in my browser returns either an ResponseAlreadySentException or an java.lang.UnsupportedOperationException with message: "Headers can no longer be set because response was already completed". The error response can flip between the two while I'm tinkering with different attempted solutions, but they seem to be saying the same thing: The call has already been responded to before I attempt to call call.respond(...).
If I change my Route instead to call the Core.onEvent() directly, hitting /foo returns "bar" in my browser as is the intended behaviour:
get("/foo") {
core.onEvent(CoreEvent.FooEvent(call))
}
For completeness, my dependency versions are:
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10"
implementation "io.ktor:ktor-server-netty:1.4.1"
Thank you in advanced for any insight you can offer.