how to handle network exception using coroutine? - android

I try handling exception using coroutine. I wrote code like this, but didn't work. I can't see any log except for using try-catch. I do not want to use try catch at all function, but want to make clean code handling exception. what should I do for this?
viewmodel
private val handler = CoroutineExceptionHandler { _, exception ->
when (exception) {
is UnknownHostException -> {
showLog("login UnknownHostException : " +exception.message)
}
else -> {
}
}
}
fun login(mobile:String){
viewModelScope.launch(handler) {
try{
var login = apiRepository.login(mobile)
_isLogin.value = login
}catch(e:Exception){
}
}
}
repository
override suspend fun login(mobile: String): LoginResultData {
var result =LoginResultData()
withContext(ioDispatcher){
val request = apiServerModel.login(mobile)
val response = request.await()
result = response
}
return result
}

fun login(mobile:String){
viewModelScope.launch(handler) {
val login = apiRepository.login(mobile)
_isLogin.value = login
}
}

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)

No exception/error when no internet coroutine + retrofit

I have the following setup
Service
// ItunesService
suspend fun searchItunesPodcast(#Query("term") term: String): Response<PodcastResponse>
Repository
// ItunesRepo
override suspend fun searchByTerm(term: String) = withContext(ioDispatcher) {
return#withContext itunesService.searchItunesPodcast(term)
}
ViewModel
fun searchPodcasts(term: String) {
viewModelScope.launch {
_res.value = Result.loading()
try {
val response = itunesRepo.searchByTerm(term)
if (response.isSuccessful) { // Nothing from here when no internet
_res.value = Result.success(response.body())
} else {
_res.value = Result.error(response.errorBody().toString())
}
} catch (e: Exception) {
_res.value = Result.exception(e)
}
}
}
Everything works great until i turn off mobile data/internet on my testing device. _res value stuck on Loading state. I have tried adding break point at if (response.isSuccessful) when there is no internet and it seams like val response = itunesRepo.searchByTerm(term) never returns how can I fix this
I switched to using Flow api on my Repository
override suspend fun searchPodcasts(term: String) = flow {
emit(Result.Loading)
try {
val res = itunesService.searchItunesPodcast(term)
if (res.isSuccessful)
emit(Result.Success(res.body()))
else
emit(Result.Error("Generic error: ${res.code()}"))
} catch (e: Exception) {
emit(Result.Error("Unexpected error", e))
}
}.flowOn(ioDispatcher)
Then collect the results on my ViewModels

Fragment popbackstack trigger lifecyclescope collect

Situation
I submit data setTripDeliver, the collect works fine (trigger LOADING and then SUCCESS). I pressed a button go to next fragment B (using replace). After that, I press back button (using popbackstack). the collect SUCCESS triggered.
Codes Related
These codes at the FragmentA.kt inside onViewCreated.
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
}
These codes when to submit data at a button setOnClickListener.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.setTripDeliver(
verificationCode,
remark
)
}
Method to collect flow collectTripReattempt()
private suspend fun collectTripReattempt() {
viewModel.tripReattempt.collect {
when (it) {
is Resource.Initialize -> {
}
is Resource.Loading -> {
Log.i("???","collectTripReattempt loading")
handleSaveEarly()
}
is Resource.Success -> {
val error = it.data?.error
if (error == null) {
Tools.showToast(requireContext(), "Success Reattempt")
Log.i("???","collectTripReattempt Success")
} else {
Tools.showToast(requireContext(), "$error")
}
handleSaveEnding()
}
is Resource.Error -> {
handleSaveEnding()
}
}
}
}
Below codes are from ViewModel.
private val _tripDeliver =
MutableStateFlow<Resource<TripDeliverResponse>>(Resource.Initialize())
val tripDeliver: StateFlow<Resource<TripDeliverResponse>> = _tripDeliver
This method to call repository.
suspend fun setTripDeliver(
verificationCode: String?,
remark: String?
) {
_tripDeliver.value = Resource.Loading()
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.value = result
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.value =
Resource.Error(messageInt = R.string.no_internet_connection)
else -> _tripDeliver.value =
Resource.Error("Trip Deliver Error: " + e.message)
}
}
}
Logcat
2021-07-09 19:56:10.946 7446-7446/com.package.app I/???: collectTripReattempt loading
2021-07-09 19:56:11.172 7446-7446/com.package.app I/???: collectTripReattempt Success
2021-07-09 19:56:17.703 7446-7446/com.package.app I/???: collectTripReattempt Success
As you can see, the last Success is called again AFTER I pressed back button (popbackstack)
Question
How to make it trigger once only? Is it the way I implement it is wrong? Thank you in advance.
This is not problem of your implementation this is happening because of stateIn() which use used in your viewModel to convert regular flow into stateFlow
If according to your code snippet the success is triggered once again, then why not loading has triggered?
as per article, it is showing the latest cached value when you left the screen and came back you got the latest cached value on view.
Resource:
https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
The latest value will still be cached so that when the user comes back to it, the view will have some data immediately.
I have found the solution, thanks to #Nurseyit Tursunkulov for giving me a clue. I have to use SharedFlow.
At the ViewModel, I replace the initialize with these:
private val _tripDeliver = MutableSharedFlow<Resource<TripDeliverResponse>>(replay = 0)
val tripDeliver: SharedFlow<Resource<TripDeliverResponse>> = _tripDeliver
At the replay I have to use 0, so this SharedFlow will trigger once. Next, change _tripDeliver.value to _tripDeliver.emit() like the codes below:
fun setTripDeliver(
verificationCode: String?,
remark: String?
) = viewModelScope.launch {
_tripDeliver.emit(Resource.Loading())
if (verificationCode == null && remark == null) {
_tripDeliver.emit(Resource.Error("Remark cannot be empty if verification is empty"))
return#launch
}
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark,
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.emit(result)
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.emit(Resource.Error(messageInt = R.string.no_internet_connection))
else -> _tripDeliver.emit(Resource.Error("Trip Deliver Error: " + e.message))
}
}
}
I hope this answer will help the others also.
I think this is because of coldFlow, you need a HotFlow. Another option is to try to hide and show fragment, instead of replacing. And yet another solution is to keep this code in viewModel.
In my opinion, I think your way of using coroutines in lifeScope is incorrect. After the lifeScope status of FragmentA is at Started again, the coroutine will be restarted:
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
So I think: You need to modify this way:
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}

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
}
}

Testing suspend corotuine

I am trying to use coroutines to handle asynchronous code for my login service. Unfortunately, the implementation of the login service must accept callbacks when it completes. I do not want this login() function to complete until one of these callbacks occurs.
Here is what I have:
fun login(): Outcome = runBlocking {
suspendCoroutine<Outcome> { continuation ->
loginService.login(
onLoginSuccess = {
// do some stuff
continuation.resume(Outcome.SUCCESS)
},
onLoginFailure = {
// handle failure case
continuation.resume(Outcome.FAILURE)
}
)
}
}
My issue is my tests never complete. I think what is happening is that the continuation block itself isn't running. I tried wrapping the call to uut.login() in a runBlocking as well, but it didn't help. Here is my test code (using Spek):
describe("when login") {
val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
var result: Outcome? = null
beforeEachTest {
doNothing().whenever(mockLoginService)?.login(capture(successCaptor), capture(failureCaptor))
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
describe("and the login succeeds") {
beforeEachTest {
successCaptor.value.invoke()
}
// other tests...
it("returns an outcome of SUCCESS") {
expect(result).to.equal(Outcome.SUCCESS)
}
}
describe("and the login fails") {
beforeEachTest {
failureCaptor.value.invoke()
}
// other tests...
it("returns an outcome of FAILURE") {
expect(result).to.equal(Outcome.FAILURE)
}
}
}
Basically, I'd like to assert that the login() method returned either a SUCCESS or FAILURE outcome based on what occurred.
Any ideas?
Of course, I figured this out right after posting. If interested, here is what I did in the test:
describe("when login") {
val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
var result: Outcome? = null
describe("and the login succeeds") {
beforeEachTest {
whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
successCaptor.value.invoke()
}
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
it("returns an outcome of SUCCESS") {
expect(result).to.equal(Outcome.SUCCESS)
}
}
describe("and the login fails") {
beforeEachTest {
whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
failureCaptor.value.invoke()
}
result = uut?.execute()
}
it("logs in with the login service") {
verify(mockLoginService)?.login(any(), any())
}
// other tests
it("returns an outcome of FAILURE") {
expect(result).to.equal(Outcome.FAILURE)
}
}
}

Categories

Resources