I'm trying to use the Android MVVM pattern with a repository class and Retrofit for network calls. I have the common problem that I can't get the coroutine to wait for the network response to return.
This method is in my ViewModel class:
private fun loadConfigModel() {
val model = runBlocking {
withContext(Dispatchers.IO) {
configModelRepository.getConfigFile()
}
}
configModel.value = model
}
In ConfigModelRepository, I have this:
suspend fun getConfigFile(): ConfigModel {
val configString = prefs.getString(
ConfigViewModel.CONFIG_SHARED_PREF_KEY, "") ?: ""
return if (configString.isEmpty() || isCacheExpired()) {
runBlocking { fetchConfig() }
} else {
postFromLocalCache(configString)
}
}
private suspend fun fetchConfig(): ConfigModel {
return suspendCoroutine { cont ->
dataService
.config() // <-- LAST LINE CALLED
.enqueue(object : Callback<ConfigModel> {
override fun onResponse(call: Call<ConfigModel>, response: Response<ConfigModel>) {
if (response.isSuccessful) {
response.body()?.let {
saveConfigResponseInSharedPreferences(it)
cont.resume(it)
}
} else {
cont.resume(ConfigModel(listOf(), listOf()))
}
}
override fun onFailure(call: Call<ConfigModel>, t: Throwable) {
Timber.e(t, "config fetch failed")
cont.resume(ConfigModel(listOf(), listOf()))
}
})
}
}
My code runs as far as dataService.config(). It never enters onResponse or onFailure. The network call goes and and returns properly (I can see this using Charles), but the coroutine doesn't seem to be listening for the callback.
So, my question is the usual one. How can I get the coroutines to block such that they wait for this callback from Retrofit? Thanks.
The problem must be that response.body() returns null since that is the only case that is missing a call to cont.resume(). Make sure to call cont.resume() also in that case and your code should at least not get stuck.
But like CommonsWare points out, even better would be to upgrade to Retrofit 2.6.0 or later and use native suspend support instead of rolling your own suspendCoroutine logic.
You should also stop using runBlocking completely. In the first case, launch(Dispatchers.Main) a coroutine instead and move configModel.value = model inside of it. In the second case you can just remove runBlocking and call fetchConfig() directly.
Related
My aim to call five apis and to get headers from those api response.
I have added my code below
Api service class
#GET("users")
suspend fun getUserList(): Call<List<FriendListModel>>
Repo class
suspend fun getList(): Response<List<FriendListModel>> {
return apiService.getUserList().execute()
}
ViewModel class
fun getFriends() {
viewModelScope.launch(Dispatchers.IO) {
val data =
async {
try {
val data = friendListRepo.getList()
val header = data.headers().get("id")
/*
* need to add header logic
*/
Resource.success(data)
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.error(false, throwable.response()?.message()?:"")
}
else -> {
Resource.error(false, "")
}
}
}
}
val res = data.await()
mutableFriendsList.postValue(res)
}
}
My question is, am I doing it in right way because I am getting a warning in repo class saying that "Inappropriate blocking method call" since I am calling execute() method though I am calling it in suspend function.
[I referred] Kotlin coroutines await for 2 or more different concurrent requests.
Is there any other approach to achieve this?
You should not combine suspend with Call. Call is for asynchronous work. suspend does asynchronous work synchronously by suspending. It can't be both at once. execute does a blocking synchronous fetch of the data, which shouldn't be done in a coroutine.
So, your functions should look like:
#GET("users")
suspend fun getUserList(): List<FriendListModel>
suspend fun getList(): List<FriendListModel> {
return apiService.getUserList()
}
Then when you use it in a coroutine, you don't need async because you're just calling a synchronous suspend function. You also don't need to fool with Dispatchers.IO since you're only using a suspend function (not doing blocking work). I also simplified your catch block in this example, but that's not related to the solution (I just couldn't help myself).
fun getFriends() {
viewModelScope.launch {
mutableFriendsList.value = try {
val data = friendListRepo.getList()
val header = data.headers().get("id")
/*
* need to add header logic
*/
Resource.success(data)
} catch (throwable: Throwable) {
Resource.error(false, (throwable as? HttpException)?.response()?.message.orEmpty())
}
}
}
Side note, even when you are calling blocking code, you should never need to use async immediately followed by an await() call on it. That is just a convoluted alternative to withContext.
I have a suspendCoroutine in my repository with which I want to send data back to my ViewModel -
suspend fun sendPasswordResetMail(emailId: String): Boolean {
return withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
firebaseAuth?.sendPasswordResetEmail(emailId)
?.addOnCompleteListener {
cont.resume(it.isSuccessful)
}
?.addOnFailureListener {
cont.resumeWithException(it)
}
}
}
}
However, neither of the listeners are called. Debugger says no executable code found at line where 'cont.resume(it.isSuccessful)' or 'cont.resumeWithException(it)' are.
I tried 'Dispatchers.IO', 'Dispatchers.Main' and 'Dispatchers.Default' but none of them seem to work. What could I be doing wrong?
My ViewModel code -
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
and
fragment -
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
I believe you are calling
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
this piece of code everytime for sending email
and
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
this piece only once.
Assuming that's true what you are essentially observing is the initial live data that was created with the model while it is being replaced everytime when resent is called. Instead call
isEmailSent.postValue(firebaseAuthRepo.sendPasswordResetMail(emailId))
from inside of a coroutine.
Also for the debugger not showing anything try adding a log above the cont.resume call and cont.resumeWithException call since it has worked for me in the past.
I think the easier way to achieve this is by using firebase-ktx and the await() function (which does what you are trying under the hood):
suspend fun sendPasswordResetMail(emailId: String): Boolean {
try {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
return true
} catch(e: Exception) {
return false
}
}
Another way would be to use flow:
suspend fun sendPasswordResetMail(emailId: String): Boolean = flow<Boolean {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
emit(true)
}.catch { e: Exception -> handleException(e) }
You could then observe this in your fragment by putting the code inside your viewmodel and calling .asLiveData()
I am making a network repository that supports multiple data retrieval configs, therefore I want to separate those configs' logic into functions.
However, I have a config that fetches the data continuously at specified intervals. Everything is fine when I emit those values to the original Flow. But when I take the logic into another function and return another Flow through it, it stops caring about its coroutine scope. Even after the scope's cancelation, it keeps on fetching the data.
TLDR: Suspend function returning a flow runs forever when currentCoroutineContext is used to control its loop's termination.
What am I doing wrong here?
Here's the simplified version of my code:
Fragment calling the viewmodels function that basically calls the getData()
lifecycleScope.launch {
viewModel.getLatestDataList()
}
Repository
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
//It worked fine when fetchContinuously was ingrained to here and emitted directly to the current flow
//And now it keeps on running eternally
fetchContinuously().collect { updatedList ->
emit(updatedList)
}
}
}
}
}
//Note logic of this function is greatly reduced to keep the focus on the problem
private suspend fun fetchContinuously(): Flow<List<Data>>
{
return flow {
while (currentCoroutineContext().isActive)
{
val updatedList = fetchDataListOverNetwork().await()
if (updatedList != null)
{
emit(updatedList)
}
delay(refreshIntervalInMs)
}
Timber.i("Context is no longer active - terminating the continuous-fetch coroutine")
}
}
private suspend fun fetchDataListOverNetwork(): Deferred<List<Data>?> =
withContext(Dispatchers.IO) {
return#withContext async {
var list: List<Data>? = null
try
{
val response = apiService.getDataList().execute()
if (response.isSuccessful && response.body() != null)
{
list = response.body()!!.list
}
else
{
Timber.w("Failed to fetch data from the network database. Error body: ${response.errorBody()}, Response body: ${response.body()}")
}
}
catch (e: Exception)
{
Timber.w("Exception while trying to fetch data from the network database. Stacktrace: ${e.printStackTrace()}")
}
finally
{
return#async list
}
list //IDE is not smart enough to realize we are already returning no matter what inside of the finally block; therefore, this needs to stay here
}
}
I am not sure whether this is a solution to your problem, but you do not need to have a suspending function that returns a Flow. The lambda you are passing is a suspending function itself:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T> (source)
Here is an example of a flow that repeats a (GraphQl) query (simplified - without type parameters) I am using:
override fun query(query: Query,
updateIntervalMillis: Long): Flow<Result<T>> {
return flow {
// this ensures at least one query
val result: Result<T> = execute(query)
emit(result)
while (coroutineContext[Job]?.isActive == true && updateIntervalMillis > 0) {
delay(updateIntervalMillis)
val otherResult: Result<T> = execute(query)
emit(otherResult)
}
}
}
I'm not that good at Flow but I think the problem is that you are delaying only the getData() flow instead of delaying both of them.
Try adding this:
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
fetchContinuously().collect { updatedList ->
emit(updatedList)
delay(refreshIntervalInMs)
}
}
}
}
}
Take note of the delay(refreshIntervalInMs).
I would like my app users to be able to cancel file upload.
My coroutine upload job in ViewModel looks like this
private var uploadImageJob: Job? = null
private val _uploadResult = MutableLiveData<Result<Image>>()
val uploadResult: LiveData<Result<Image>>
get() = _uploadResult
fun uploadImage(filePath: String, listener: ProgressRequestBody.UploadCallbacks) {
//...
uploadImageJob = viewModelScope.launch {
_uploadResult.value = withContext(Dispatchers.IO) {
repository.uploadImage(filePart)
}
}
}
fun cancelImageUpload() {
uploadImageJob?.cancel()
}
Then in the repository the Retrofit 2 request is handled like this
suspend fun uploadImage(file: MultipartBody.Part): Result<Image> {
return try {
val response = webservice.uploadImage(file).awaitResponse()
if (response.isSuccessful) {
Result.Success(response.body()!!)
} else {
Result.Error(response.message(), null)
}
} catch (e: Exception) {
Result.Error(e.message.orEmpty(), e)
}
}
When cancelImageUpload() it called the job gets cancelled and the exception gets caught in the repository but the result won't get assigned to uploadResult.value.
Any ideas please how to make this work?
PS: There is a similar question Cancel file upload (retrofit) started from coroutine kotlin android but it suggests using coroutines call adapter which is depricated now.
Have finally managed to make it work by moving withContext one level up like this
uploadImageJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
_uploadResult.postValue(repository.uploadImage(filePart))
}
}
I am building an app based off of the Android Clean Architecture Kotlin version (https://github.com/android10/Android-CleanArchitecture-Kotlin).
Using this architecture, each time you want to invoke a use case, a Kotlin coroutine is launched and the result is posted in the main thread. This is achieved by this code:
abstract class UseCase<out Type, in Params> where Type : Any {
abstract suspend fun run(params: Params): Either<Failure, Type>
fun execute(onResult: (Either<Failure, Type>) -> Unit, params: Params) {
val job = async(CommonPool) { run(params) }
launch(UI) { onResult.invoke(job.await()) }
}
In his example architecture, Mr. Android10 uses Retrofit to make a synchronous api call inside the kotlin couroutine. For example:
override fun movies(): Either<Failure, List<Movie>> {
return when (networkHandler.isConnected) {
true -> request(service.movies(), { it.map { it.toMovie() } }, emptyList())
false, null -> Left(NetworkConnection())
}
}
private fun <T, R> request(call: Call<T>, transform: (T) -> R, default: T): Either<Failure, R> {
return try {
val response = call.execute()
when (response.isSuccessful) {
true -> Right(transform((response.body() ?: default)))
false -> Left(ServerError())
}
} catch (exception: Throwable) {
Left(ServerError())
}
}
'Either' represents a disjoint type, meaning the result will either be a Failure or the object of type T you want.
His service.movies() method is implemented like so (using retrofit)
#GET(MOVIES) fun movies(): Call<List<MovieEntity>>
Now here is my question. I am replacing retrofit with Google Cloud Firestore. I know that currently, Firebase/Firestore is an all async library. I want to know if anyone knows of a method more elegant way of making a synchronous API call to Firebase.
I implemented my own version of Call:
interface Call<T: Any> {
fun execute(): Response<T>
data class Response<T>(var isSuccessful: Boolean, var body: T?, var failure: Failure?)
}
and my API call is implemented here
override fun movieList(): Call<List<MovieEntity>> = object : Call<List<MovieEntity>> {
override fun execute(): Call.Response<List<MovieEntity>> {
return movieListResponse()
}
}
private fun movieListResponse(): Call.Response<List<MovieEntity>> {
var response: Call.Response<List<MovieEntity>>? = null
FirebaseFirestore.getInstance().collection(DataConfig.databasePath + MOVIES_PATH).get().addOnCompleteListener { task ->
response = when {
!task.isSuccessful -> Call.Response(false, null, Failure.ServerError())
task.result.isEmpty -> Call.Response(false, null, MovieFailure.ListNotAvailable())
else -> Call.Response(true, task.result.mapTo(ArrayList()) { MovieEntity.fromSnapshot(it) }, null)
}
}
while (response == null)
Thread.sleep(50)
return response as Call.Response<List<MovieEntity>>
}
Of course, the while loop at the end bothers me. Is there any other, more elegant ways, to wait for the response to be assigned before returning from the movieListResponse method?
I tried calling await() on the Task that is returned from the Firebase get() method, but the movieListResponse method would return immediately anyway. Thanks for the help!
So I found what I was looking for in the Google Tasks API: "If your program is already executing in a background thread you can block a task to get the result synchronously and avoid callbacks" https://developers.google.com/android/guides/tasks#blocking
So my previous problematic code becomes:
private fun movieListResponse(): Call.Response<List<MovieEntity>> {
return try {
val taskResult = Tasks.await(FirebaseFirestore.getInstance().
collection(DataConfig.databasePath + MOVIES_PATH).get(), 2, TimeUnit.SECONDS)
Call.Response(true, taskResult.mapTo(ArrayList()) { MovieEntity.fromSnapshot(it) }, null)
} catch (e: ExecutionException) {
Call.Response(false, null, Failure.ServerError())
} catch (e: InterruptedException) {
Call.Response(false, null, Failure.InterruptedError())
} catch (e: TimeoutException) {
Call.Response(false, null, Failure.TimeoutError())
}
}
Note I no longer need my Thread.sleep while loop.
This code should only be run in a background thread/kotlin coroutine.
This is overengineered, there are several layers trying to do the same thing. I suggest you go back a few steps, undo the abstractions and get into the mood of using coroutines directly. Implement a suspend fun according to this template. You don't need the crutches of Either, handle exceptions in the most natural way: a try-catch around a suspend fun call.
You should end up with a signature as follows:
suspend fun movieList(): List<MovieEntity>
Call site:
launch(UI) {
try {
val list = movieList()
...
} catch (e: FireException) {
// handle
}
}
That's is not the way how firebase works. Firebase is based on callback.
I recommend architecture component's livedata.
Please check the following example.
here is a link: https://android.jlelse.eu/android-architecture-components-with-firebase-907b7699f6a0