I am trying to run a test in which I want to wait till higher order function executes. As of now I am not able to figure out any ways to do it. Following is my code.
#Test
fun `test execute routine error`() = runBlocking(coroutineDispatcher) {
val observer = mock<Observer<String>>()
baseViewModel.error.observeForever(observer)
val httpException = HttpException(Response.error<String>(402, mock(ResponseBody::class.java)))
val function = baseViewModel.executeRoutine {
throw httpException
}
verify(observer).onChanged("Something went wrong. Please try again")
}
The problem with above snippet is that it jumps to the last line i.e. verify() before throwing an http exception for executeRoutine.
Update: Execute routine definition
fun executeRoutine(requestType: RequestType = RequestType.POST_LOGIN, execute: suspend () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
_spinner.postValue(true)
try {
execute()
} catch (ex: HttpException) {
val errorHandler = errorHandlerFactory.create(requestType)
_error.postValue(errorHandler.getErrorMessageFrom(ex))
} catch (ex: Exception) {
_error.postValue(ex.localizedMessage)
Timber.e(ex)
} finally {
_spinner.postValue(false)
}
}
}
}
The problem is that the higher order function does execute, it just doesn't do what you think it does -- its execution is launching the task, not waiting for it to complete.
You will have to solve the problem another way, by either having your test wait until the change is observed, or having the callback complete a barrier to allow the test to proceed (e.g. completableJob.complete() at the end of the call back, and completableJob.join() waiting before proceeding with the test).
It might also be desirable to rearchitect your code so you don't have to do anything special, e.g. by making executeRoutine a suspend function executing the code rather than launching the code in another scope.
Related
I'm using Firebase authentication. In the repository I have this function:
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
This function is called from within the ViewModel class:
var response by mutableStateOf<Result<Boolean>>(Result.Success(false))
private set
fun signIn() = viewModelScope.launch {
response = repository.signIn()
}
Which works fine but I was suggested to use in the ViewModel:
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
To add Dispatchers.IO and inside the repository:
override suspend fun signIn(): Result<Boolean> {
return withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
To launch a coroutine using withContext. I and I don't understand why? I'm using Jetpack Compose.
Whoever suggested changing your code is wrong.
It is a general Kotlin coroutines convention that suspend functions never need to be called on a specific dispatcher, specifically because they must never block. They always internally delegate to a specific dispatcher if they need one. (But perhaps as an optimization, a private suspend function might avoid doing it for a function that must be called on the Main dispatcher.)
Since this is a convention, all the libraries from Google, Android, Square, etc. and anyone else who knows what they're doing, only have suspend functions that can be called from any dispatcher.
This includes the await() call you're using with Firebase. Therefore, your repository's signIn function is already perfectly fine as-is. Since it doesn't call any blocking functions, and the suspend function it calls is a proper suspend function that also does not block, it conforms to the standard (it doesn't block).
The function in your ViewModel is also fine. No dispatcher needs to be specified.
Actually, since you are already calling signIn from a coroutine started with Dispatchers.IO you don't have to use return withContext(...).
Since your repository method is suspend, it is able to call coroutines without special blocks like withContext.
// This line tells to launch code on separate IO thread, to avoid UI freezing
// Since default viewModelScope.launch runs on Dispatchers.Main, which is
// also used for rendering
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
In your repository you can just
// Since signIn was called on IO context from viewModel, it will also
// return on IO
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
We have a two ways to start coroutine async and launch.
launch will use to perform serial/sequence task in background.
async is used when we expect some result back and also want to perform parallel operation.
Same way withContext is nothing but another way of writing the async where one does not have to write await(). When withContext, is used, it runs the tasks in series instead of parallel. So one should remember that when we have a single task in the background and want to get back the result of that task, we should use withContext.
In your case you can change your code as below
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
val response = async { repository.signIn()}.await()
}
and remove withContext
suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
One more way if you don't want to use return with withContext
override suspend fun signIn() = {
withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
In Nutshell if you expecting some result from your task then you have to use async or withContext.
Hope I am able to solve your problem or issue.
I'm trying to implement One Tap, so I have created this function:
override fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
emit(Result.Error(e.message))
}
}
//.flowOn(Dispatchers.IO)
And some programmer told me that I need to add .flowOn(Dispatchers.IO) to the above function, so it can be correct. My code work correct without it. Here is how I call this function in the ViewModel:
fun oneTapSignIn() = viewModelScope.launch {
repo.oneTapSignInWithGoogle().collect { response ->
oneTapSignInResponse = response
}
}
Is it really necessary to do that? I'm really confused.
You're calling beginSignIn which returns a Task, so it does its own stuff in the background. Now Task.await is suspending, not blocking, so it won't block the current thread while waiting for the task.
Therefore, the body of your flow doesn't contain any blocking stuff, so there is no reason to use flowOn(Dispatchers.IO) here.
I try to implement deleting user in FirebaseAuth using Kotlin flow (SharedFlow).
In onDeleteAccountClicked() there is delete() method called from FirebaseAuth which may throw AuthReauthenticationRequiredException. When the exception is thrown, app redirects to another fragment to reauthenticate, then call onDeleteAccountClicked() once again, but flow emits nothing.
ViewModel
private val _deleteAccount = MutableSharedFlow<() -> Unit>()
fun onDeleteAccountClicked() {
logd("outside the viewModelScope")
viewModelScope.launch {
logd("inside the viewModelScope")
_deleteAccount.emit {
logd("emitting log")
firebaseAuth.deleteUser()
//throw AuthReauthenticationRequiredException()
}
}
}
init {
viewModelScope.launch {
_deleteAccount
.onEach {
it()
}
.catch {
if (it is AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
.collect()
}
}
Logs "outside the viewModelScope" and "inside the viewModelScope" shows every time when the method is called, but "emitting log" only for the first time.
Am I even trying to do it the right way?
I just tested the code, and it works for me. I called onDeleteAccountClicked() three times with delay between calling, and all three logs "emitting log" inside emit lambda were printed. Try to remove calling firebaseAuth.deleteUser() inside emit lambda and test. Calling FirebaseUser.delete function when user is already deleted throws FirebaseAuthInvalidUserException exception. Maybe that's why you didn't see logs - because FirebaseUser.delete function throws an exception.
I think the structure you use for calling just one function is a bit complicated, I can suggest to get rid of _deleteAccount flow and just wrap firebaseAuth.deleteUser() inside try-catch (you even don't need to launch a coroutine for that):
fun onDeleteAccountClicked() {
try {
firebaseAuth.deleteUser()
} catch(e: AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
In my sample I'm calling network operation and emitting success case but on error e.g 404 app crashes wihout emitting exception. Surrendering with try catch prevent crashes but I want to pass error till the ui layer like success case.
suspend fun execute(
params: Params,
):
Flow<Result<Type>> = withContext(Dispatchers.IO) {
flow {
emit(Result.success(run(params)))
}.catch {
emit(Result.failure(it))
}
}
There is a helpful function runCatching for creating a Result easily, but the problem in coroutines is that you don't want to be swallowing CancellationExceptions. So below, I'm using runCatchingCancellable from my answer here.
This shouldn't be a Flow since it returns a single item.
If run is a not a blocking function (it shouldn't be if you are using Retrofit with suspend functions), your code can simply be:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
run(params)
}
If it is a blocking function you can use:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
withContext(Dispatchers.IO) {
run(params)
}
}
If you were going to return a Flow (which you shouldn't for a returning a single item!!), then you shouldn't make this a suspend function, and you should catch the error inside the flow builder lambda:
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
run(params)
})
}
// or if run is blocking (it shouldn't be):
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
withContext(Dispatchers.IO) { run(params) }
})
}
If you want to use flows you can use the catch method of flows.
As you said you can use try-catch but it would break the structured concurrency since it would catch the cancellation exception as well or it would avoid the cancellation exception to be thrown.
One thing that you can do is to use an Exception handler at the point where you launch the root coroutine that calls the suspend function.
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
// handle it
}
scope.launch(handler) { // root coroutine
execute(params)
somethingThatShouldBeExecutedOnlyIfPreviousCallDoesNotThrow()
}
This solution is good for both flows and non-flow coroutines.
In the solution with the runCatching you will have to manually check the result of the first execute to avoid the second one to run.
One interesting thread is here.
Trying to get a deeper into coroutines. I have a suspendCancellableCoroutine that is supposed to fetch a network response. I can see in Charles that the network call is dispatched and returns successfully. However, my app just hangs on the network request line.
private suspend fun fetchVisualElementsFromServer(clubId: String): VisualElements {
return suspendCancellableCoroutine { cont ->
visualElementsService.fetchVisualElementsForClub(clubId)
.enqueue(object : Callback<ResultVisualElements> {
override fun onResponse(
call: Call<ResultVisualElements>,
response: Response<ResultVisualElements>
) {
if (response.isSuccessful) {
response.body()?.let {
if (it.result == RESULT_SUCCESS) {
saveVisualElementsResponseInSharedPreferences(it.visual_elements)
cont.resume (it.visual_elements)
} else {
cont.cancel() //edit
}
} ?: cont.cancel() //edit
} else {
cont.cancel(IOException("${response.code()}: ${response.errorBody()}"))
}
}
override fun onFailure(call: Call<ResultVisualElements>, t: Throwable) {
Timber.e(t, "visual elements fetch failed")
cont.cancel() // edit
}
})
}
}
This where it hangs:
VisualElementsService.kt
fun fetchVisualElementsForClub(clubId: String): Call<ResultVisualElements> {
return dataFetcherService.getVisualElementsForClub(clubId)
}
What am I missing here? I tried to make the fetchVisualElementsForClub() a suspend function, but that just makes the suspendCancellableCoroutine throw a Suspension functions can only be called within coroutine body error. But I thought that his was within a coroutine body?
Any help appreciated. Thanks.
EDIT
I response to Rene's answer below, I want to add a few things.
You are right, I am missing three cont.cancel() calls. I've modified the OP. Good points.
I have breakpoints all over the suspendCancellableCoroutine such that any possible scenario (success, failure, etc.) will be hit. But that callback never registers.
Wondering if there is something missing in fetchVisualElementsForClub() that is needed to pass the callback up to the suspendCancellableCoroutine. That seems to be where this is hanging.
You must call cont.resume() or cont.cancel() on every branch in your callback handling.
But in your example at least three cases are missing.
If the response is successful but no body is provided, you call nothing.
If the response is successful, the body is not null, but the it.result is not RESULT_SUCCESS you call nothing.
If something goes wrong in onFailure, you call nothing.
As long as neither resume or cancel is invoked, the coroutine will stay suspended, means hangs.
when you use suspend keyword your are telling that function shoud be called inside a coroutine bode, for example:
suspend fun abc(){
return
}
when you want to call above function you have to call it inside coroutines such as below:
GlobalScope.launch {
abc()
}