suspend IO function never return - android

I have difficulties writing an UDP message receive loop for Android.
In the following code, in receiveLoop, the call to receiveMessages never returns and I therefore never enter the message treatment loop.
Note that I am still able to receive packets, but it stops when the channel buffer is full.
I would expect receiveMessages to return immediately, while the blocking IO loop inside it would still run forever.
class MySocketUDP(private val params: SocketParams) {
private val rcvSocket: DatagramSocket by lazy {
val sock = DatagramSocket(params.rcvPort)
sock.reuseAddress = true
sock.soTimeout = 1000
sock
}
suspend fun receiveMessages(channel: SendChannel<Message>) {
withContext(Dispatchers.IO) {
val buf = ByteArray(MAX_MSG_SIZE)
while (true) {
val pkt = DatagramPacket(buf, buf.size)
try {
if (channel.isClosedForSend) {
break
}
rcvSocket.receive(pkt)
val msg = packetToMessage(buf, 0, pkt.length)
Log.d("SOCKET", "filling channel with $msg")
channel.send(msg)
} catch (ex: SocketTimeoutException) {
} catch (ex: CancellationException) {
break
}
}
}
}
}
class MyModel {
private suspend fun receiveLoop(socket: MySocketUDP) {
withContext(Dispatchers.Main) {
val channel = Channel<Message>(16)
socket.receiveMessages(channel)
Log.d("MODEL", "Entering msg loop")
for (msg in channel) {
dispatchRcvMessage(msg)
}
}
}
}
Why does receiveMessages never return while it is running in the IO dispatcher and called from the Main dispatcher?
Do I need to actually spawn a thread to such producer/consumer work?
Can you show how to achieve such long blocking code nicely in a "coroutine-friendly" manner?
Thank you

receiveMessages() is a suspend function which calls another suspend function withContext(), which in turn has an infinite loop. So calling socket.receiveMessages(channel) will suspend code execution while the loop is not finished.
You need to launch separate coroutines for consumer and producer, e.g. using launch function.
Some example of using coroutines:
val someScope = CoroutineScope(Dispatchers.Main)
private suspend fun receiveLoop(socket: MySocketUDP) = someScope.launch {
val channel = Channel<Message>(16)
socket.receiveMessages(channel)
Log.d("MODEL", "Entering msg loop")
for (msg in channel) {
dispatchRcvMessage(msg)
}
}
// In MySocketUDP
suspend fun receiveMessages(channel: SendChannel<Message>) {
someAnotherScope.launch { // or can use coroutineScope builder function
val buf = ByteArray(MAX_MSG_SIZE)
while (true) {
val pkt = DatagramPacket(buf, buf.size)
try {
if (channel.isClosedForSend) {
break
}
rcvSocket.receive(pkt)
val msg = packetToMessage(buf, 0, pkt.length)
Log.d("SOCKET", "filling channel with $msg")
channel.send(msg)
} catch (ex: SocketTimeoutException) {
} catch (ex: CancellationException) {
break
}
}
}
}

Related

Getting error "Suspension functions can be called only within coroutine body" in Kotlin

I've written one function with Flow collector which is as shown below,
private fun callSocket(
eventEmmit: String,
eventOn: String,
request: JSONObject
): Flow<SocketCallback<JSONObject>> =
flow {
try {
if (socket.connected()) {
var response = JSONObject()
Log.e("EMIT", JSONObject(Gson().toJson(request)).toString())
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
**this.emit(SocketCallback.OnSuccess(response))**
}.emit(
eventEmmit,
request
)
emit(SocketCallback.OnSuccess(response))
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
emit(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
emit(SocketCallback.OnError(e.toString()))
}
}.flowOn(Dispatchers.IO)
But when I write this.emit(SocketCallback.OnSuccess(response))(enclosed in ** in code) in on method it shows me the error "Suspension functions can be called only within coroutine body".
Any solution for this?
Thanks in advance.
You are trying to emit events to flow outside of coroutineScope. socket.on() function probably has signature:
fun on(ev: String, block: (args: String) -> Unit) {
}
in that case, inside lambda block: (args: String) -> Unit) you are outside of scope and you can not invoke suspending functions.
You have only 2 solutions:
Every time new event approach - create new coroutine with coroutine builder launch:
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
launch {
emit(SocketCallback.OnSuccess(response))
}
}.emit(
eventEmmit,
request
)
Use callbackFlow to avoid creation of new coroutine on each event. Please check especially this post.
Here's how I solved it by using the CallBackFlow
private fun callOnSocket(
eventOn: String
): Flow<SocketCallback<JSONObject>> =
callbackFlow<SocketCallback<JSONObject>> {
try {
if (socket.connected()) {
Log.e("ON", "Started")
var response = JSONObject()
socket.on(
eventOn
) {
response = it[0] as JSONObject
Log.e("ON", response.toString())
trySend(SocketCallback.OnSuccess(response))
}
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
trySend(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
trySend(SocketCallback.OnError("Socket connection failed"))
}
awaitClose { cancel() }
}.flowOn(Dispatchers.IO)

How to cancel api request in view model in Mvvm?

I am try to cancel to api request if user calls api to fast then only the latest api should return the result all previous requests should be discarded but this isn't working anyone knows the solution please help thanks
class CartViewModel(val store: Account) : BaseViewModel() {
private var requestCalculation: Job? = null
fun recalculate() {
requestCalculation.let {
if (it != null) {
if (it.isActive) {
requestCalculation!!.cancel()
}
}
}
requestCalculation = viewModelScope.launch(Dispatchers.IO) {
isLoading.postValue(true)
try {
val order = CCOrderManager.shared.calculateTaxesAndApplyRewards(store.id)
refreshOrder()
} catch (e: Exception) {
exception.postValue(e.localizedMessage ?: e.toString())
}
}
}
}
The order of cancellation and execution is wrong. When the function starts, requestCalculation is null, so it cannot be canceled. Make sure you start first the coroutine and cancel it later. For example:
private var requestCalculation: Job? = null
fun recalculate() {
requestCalculation = viewModelScope.launch(Dispatchers.IO) {
delay(10_000)
// do your work...
}
// now the job can be canceled
requestCalculation?.cancel()
}
Adding a check after api call this.isActive {return#launch} finally worked for me...
fun recalculate() {
calculationRequest?.cancel()
isLoading.postValue(true)
calculationRequest = viewModelScope.launch(Dispatchers.IO) {
try {
val order =
CCOrderManager.shared.calculateTaxesAndApplyRewards(store.id)
// this check is the solution *******
if (!this.isActive) {return#launch}
val catalog = CatalogManager.shared().catalog
} catch (e: Exception) {
}
}
}

ChannelFlow producer scope is not triggered

I have issue with the following code:
suspend fun getData(): Flow<Resource<User>> {
println("#getData called")
return channelFlow {
println("#getData producer scope started")
launch(Dispatchers.IO) {
val cachedData = sharedPreferencesUtils.getData()
println("#getData send cached data")
send(cachedData)
}
launch(Dispatchers.IO) {
val response = handleAPI<EducationResponse> { apiService.getData() }
println("#getData send remote data")
send(response)
}
}
}
Above code is working most times and send data from local and remote, but sometimes the producer scope not triggered (it print "#getData called" only)
Please advice!
UPDATE:
and the Flow collected:
CoroutineScope(Dispatchers.IO).launch(AppExceptionHandler.exceptionHandler) {
prescriptionsRepository
.getData()
.collect {
educationWithSponsorsLiveData.postValue(it)
}
}

Multiple Retrofit calls with Flow

I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}

Use main thread after long network operation

In my android app I has long network operation. After operation is finished I need to update my ui.
So as result long operation need to execute in background thread.
Snippet:
private val isShowProgressLiveData = MutableLiveData<Boolean>()
// launch a new coroutine in background and continue
GlobalScope.launch() {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}
But I get error on
isShowProgressLiveData.value = false
Error message:
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:461)
at androidx.lifecycle.LiveData.setValue(LiveData.java:304)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
at com.myoperation.AddTraderViewModel$doClickStart$3.invokeSuspend(AddTraderViewModel.kt:55)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
I need to update my ui in main thread. So how I can fix this?
You can switch to main thread like with the withContext(Dispatchers.Main)
When you fire a coroutine with launch will start on the Dispatchers.Default. The best is to specify it like this:
GlobalScope.launch(Dispatchers.IO) {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
withContext(Dispatchers.Main){ //switched to Main thread
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
}
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}
EDIT: Or you can just use .postValue() instead of .value or .setValue() and forget about withContext().
You're still in background thread when you try to execute
isShowProgressLiveData.value = false
You can do something like this:
GlobalScope.launch() {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
YourActivity.runOnUiThread(object:Runnable() {
override fun run() {
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
}
}
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}

Categories

Resources