Android download multiple files with OkHttp and coroutine - android

In my app, I get a set of urls to some images from an api and need to create Bitmap objects out of those urls to be able do display the images in the UI. I saw that the android docs recommend using corutines for performing such async tasks, but I am not sure how to do it properly.
Using OkHttp for my http client, I tried the following approach:
GlobalScope.launch {
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
for (i in 0 until parsedRes.size) {
val bitmap =
GlobalScope.async { createBitmapFromUrl(parsedRes[i].best_book.image_url) }
parsedRes[i].best_book.imageBitmap = bitmap.await();
}
searchResults.postValue(parsedRes)
}
Where response is what I get back from my API, and searchResults is a LiveData that hold the parsed response.
Also, here is how I am getting the images from those urls:
suspend fun createBitmapFromUrl(url: String): Bitmap? {
val client = OkHttpClient();
val req = Request.Builder().url(url).build();
val res = client.newCall(req).execute();
return BitmapFactory.decodeStream(res.body?.byteStream())
}
Even though every fetch action is done on a separate coroutine, it's still too slow. Is there a better way of doing it? I can use any other http client if there is one out there optimized for use with coroutines, although I am new to Kotlin so I don't know any.

First of all the createBitmapFromUrl(url: String) does everything synchronously, you've to first stop them from blocking the coroutine thread, you may want to use Dispatchers.IO for that because callback isn't the most idomatic thing ever in coroutines.
val client = OkHttpClient() // preinitialize the client
suspend fun createBitmapFromUrl(url: String): Bitmap? = withContext(Dispatchers.IO) {
val req = Request.Builder().url(url).build()
val res = client.newCall(req).execute()
BitmapFactory.decodeStream(res.body?.byteStream())
}
Now, when you are calling bitmap.await() you are simply saying that "Hey, wait for the deferred bitmap and once it is finished resume the loop for next iteration"
So you may want to do the assignment in the coroutine itself to stop it from suspending the loop, otherwise create another loop for that. I'd go for first option.
scope.launch {
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
for (i in 0 until parsedRes.size) {
launch {
parsedRes[i].best_book.imageBitmap = createBitmapFromUrl(parsedRes[i].best_book.image_url)
}
}
}

Use a library like the following that doesn't use the blocking execute method and instead bridges from the async enqueue.
https://github.com/gildor/kotlin-coroutines-okhttp
suspend fun main() {
// Do call and await() for result from any suspend function
val result = client.newCall(request).await()
println("${result.code()}: ${result.message()}")
}
What this basically does is the following
public suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
//Ignore cancel exception
}
}
}
}

Related

How to call multiple api concurrently and read headers using Retrofit, Coroutines with Async/Await method in Kotlin Android

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.

My function is asynchronous, why do I get a NetworkOnMainThread Exception?

This is the object where all my API calls are made. Sometimes when I make a call to this function, I receive a NetworkOnMainThread exception. It doesn't happen every time. I'm confused because I've made this function asynchronous... why am I still getting this exception?
object APICaller{
private const val apiKey = "API_KEY_HERE"
//Live Data Objects
var errorCode = MutableLiveData<Int>()
var fetchedResponse = MutableLiveData<Response>()
//Asynchronous network call
suspend fun networkCall(query: String) = withContext(Dispatchers.Default){
val apiURL = "API_URL_HERE"
try{
//Get response
val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).execute()
if(response.isSuccessful){
//UI changes (including changes to LiveData values) must be performed on main thread.
Handler(Looper.getMainLooper()).post{
fetchedResponse.value = response
}.also{
Log.i("Response Succ", response.toString())
}
} else {
Handler(Looper.getMainLooper()).post{
errorCode.value =
ToastGenerator.REQUEST_ERROR
}.also{
Log.i("Response Fail", response.toString())
}
}
//Catch any thrown network exceptions whilst attempting to contact API
} catch(e: Exception){
Handler(Looper.getMainLooper()).post{
errorCode.value =
ToastGenerator.NETWORK_ERROR
}.also{
Log.i("Network Fail", e.message.toString())
}
}
}
}
There are three other classes that make utilise return value from the APICaller networkCall() function. The first is a ViewModel that references it directly.
class BrowseViewModel: ViewModel() {
//LiveData Objects
//Transformations listen to LiveData in APICaller and map it to LiveData in this ViewModel
var errorCode: LiveData<Int>? = Transformations.map(APICaller.errorCode){ code ->
return#map code
}
var obtainedResponse: LiveData<String> = Transformations.map(APICaller.fetchedResponse){ response ->
return#map response.body()?.string()
}
//Upon a search request, make a network call
fun request(query: String) {
GlobalScope.launch{
APICaller.networkCall(query)
}
}
//Convert API response to GameData object
fun handleJSONString(jsonString: String, file: String) : List<GameData>{
return DataTransformer.JSONToGameData(JSONObject(jsonString), JSONObject(file))
}
}
The second is an fragment that calls the ViewModel's function.
fun request(query: String){
browseViewModel.request(query)
progressSpinner?.visibility = View.VISIBLE
}
The third is an Activity that calls upon the Fragment's function.
private fun makeRequest(query: String){
browseFragment.let{
supportFragmentManager.beginTransaction().replace(R.id.fragmentContainer, it).commit()
it.request(query)
}
}
Could it be related to these other functions?
Any feedback is greatly appreciated. Thank you in advance :)
I don't see exactly what could go wrong in your code, but it could definitely be much cleaner, which possibly would resolve your issue, or at least make it easier to find the error.
A good strategy with suspend functions is to design them to always be called from a Main dispatcher. Then you can freely make UI calls in them and wrap only the background parts with withContext. So your above function could move the withContext() down to only wrap the execute() call, and all your Handler usage could be removed. (Incidentally, those would have been cleaner using withContext(Dispatchers.Main).)
However, Retrofit already provides a suspend function version of making a call, so you don't even need the withContext wrapper. Just use await() instead of execute() and your suspend function would collapse down to:
suspend fun networkCall(query: String) {
val apiURL = "API_URL_HERE"
try{
val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).await()
if (response.isSuccessful){
Log.i("Response Succ", response.toString())
fetchedResponse.value = response
} else {
Log.i("Response Fail", response.toString())
errorCode.value = ToastGenerator.REQUEST_ERROR
}
} catch(e: Exception){
Log.i("Network Fail", e.message.toString())
errorCode.value = ToastGenerator.NETWORK_ERROR
}
}
And then instead of using GlobalScope, you should use a viewModelScope or lifecycleScope so your coroutines don't leak UI components. And the await() suspending function above supports cancellation, so if for instance your ViewModel is destroyed due to the associated Fragment or Activity going out of scope, your network call will be cancelled automatically for you.
fun request(query: String) {
viewModelScope.launch {
APICaller.networkCall(query)
}
}
If the problem persists, study your stack trace closely to see where you might be making the error.

Cancel Retrofit request started from ViewModel coroutine job

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

Kotlin Coroutine wait for Retrofit Response

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.

Kotlin launch coroutine skips code line where Google Volley retrieves information from a server

I have this code in a fragment:
#ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
create_adoption_btn.setOnClickListener {
val temp = Intent(activity!!.baseContext, AdoptionCreationActivity::class.java)
activity!!.startActivityFromFragment(this, temp, 1)
}
val mLayoutManager = GridLayoutManager(activity!!.baseContext, 1)
recycler_view.layoutManager = mLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
//recycler_view.adapter = adapter
//AppController.instance!!.getAdoptionList().await()
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT) {
Log.i("TestAdapter", "Beginning fetch")
val adapter = AlbumsAdapter(activity!!, AppController.instance!!.getAdoptionList()) //Skips this line, but still executes it
Log.i("TestAdapter", "Adapter: ${adapter.itemCount}")
recycler_view.adapter = adapter
adapter.notifyDataSetChanged()
Log.i("TestAdapter", "Adapter updated on thread")
}
}
And this for a class that extends Application
class AppController : Application() {
private var adoptionCardList: MutableList<AdoptionCard> = mutableListOf()
override fun onCreate() {
super.onCreate()
instance = this
}
fun getAdoptionList(): MutableList<AdoptionCard> {
if(adoptionCardList.count() == 0) {
val service = GetVolley()
val apiController = ApiController(service)
val path = "adoptions/read.php"
apiController.get(path, JSONArray()){ response ->
if (response != null) {
var x = 0
while(x <= response.length() - 1){
val jsonObject = (response[x] as JSONObject)
adoptionCardList.add(AdoptionCard(
jsonObject.getInt("id"),
jsonObject.getString("adoption_title"),
jsonObject.getString("user_id").toBigInteger(),
jsonObject.getString("adoption_created_time")))
x+=1
}
}
}
}
return adoptionCardList
}
private val requestQueue: RequestQueue? = null
get() {
if (field == null) {
return Volley.newRequestQueue(applicationContext)
}
return field
}
fun <T> addToRequestQueue(request: Request<T>, tag: String) {
request.tag = if (TextUtils.isEmpty(tag)) TAG else tag
requestQueue?.add(request)
}
fun <T> addToRequestQueue(request: Request<T>) {
request.tag = TAG
requestQueue?.add(request)
}
fun cancelPendingRequests(tag: Any) {
if (requestQueue != null) {
requestQueue!!.cancelAll(tag)
}
}
companion object {
private val TAG = AppController::class.java.simpleName
#get:Synchronized var instance: AppController? = null
private set
}
The "launch" coroutine should wait until Volley retrieves all information from the server but it just skips that line and the Recycler View doesn't update, since the MutableList is empty. If I reload the Fragment, it will do this successfully since there's an already stored list. I read all documentation I could on Kotlin Coroutines and questions asked but I can't make this work. Could anyone help me?
The debug:
Debug log
On the first load, as you can see, the adapter has 0 elements, so the view gets nothing; on the second load, it already has 3 elements, so the Recycler view loads those 3.
ApiController:
class ApiController constructor(serviceInjection: RESTapi): RESTapi {
private val service: RESTapi = serviceInjection
override fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit) {
service.get(path, params, completionHandler)
}
}
Interface:
interface RESTapi {
fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit)
}
GetVolley class:
class GetVolley : RESTapi {
val TAG = GetVolley::class.java.simpleName
val basePath = "http://192.168.0.161/animals/"
override fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit) {
val jsonObjReq = object : JsonArrayRequest(Method.GET, basePath + path, params,
Response.Listener<JSONArray> { response ->
Log.d(TAG, "/get request OK! Response: $response")
completionHandler(response)
},
Response.ErrorListener { error ->
VolleyLog.e(TAG, "/get request fail! Error: ${error.message}")
completionHandler(null)
}) {
#Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
val headers = HashMap<String, String>()
headers["Content-Type"] = "application/json"
return headers
}
}
AppController.instance?.addToRequestQueue(jsonObjReq, TAG)
}
Your problem here is that Volley is async by default. What this means is that it creates a new thread to run the call on. Since you're using coroutines, this is pointless. You'll need to force it over on the active thread and do a sync call instead.
This part:
AppController.instance?.addToRequestQueue(jsonObjReq, TAG)
Adds it to a request queue. This means it doesn't execute it instantly, but queues it with other requests (if there are any), and launches it on a separate thread. This is where your problem lies. You need to use a sync request instead. Async simply means "not on this thread", regardless of which thread. So since you're using a different one (coroutine), you'll need to force it to be sync. This makes it sync with the active thread, not the main thread.
I'm not sure if this will even work with coroutines, but since it's async, it should be fine.
In order to block the thread, you can use a RequestFuture<JSONArray> as a replacement for the callbacks. You still need to add it to the request queue, but you can call .get on the RequestFuture, which blocks the thread until the request is complete, or it times out.
val future = RequestFuture.newFuture<JSONArray>() // The future
val jsonObjReq = object : JsonArrayRequest(Method.GET, basePath + path, params,
future, // This is almost identical as earlier, but using the future instead of the different callback
future) {
#Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
val headers = HashMap<String, String>()
headers["Content-Type"] = "application/json"
return headers
}
}
AppController.instance?.addToRequestQueue(jsonObjReq, TAG);// Adds it to the queue. **This is very important**
try {
// Timeout is optional, but I highly recommend it. You can rather re-try the request later if it fails
future.get(30, TimeUnit.SECONDS).let { response ->
completionHandler(response)
}
}catch(e: TimeoutException){
completionHandler(null)
// The request timed out; handle this appropriately.
}catch(e: InterruptedException){
completionHandler(null)
// The request timed out; handle this appropriately.
}catch(e: ExecutionException){
completionHandler(null)
// This is the generic exception thrown. Any failure results in a ExecutionException
}
// The rest could be thrown by the handler. I don't recommend a generic `catch(e: Exception)`
This will block the thread until the response is received, or it times out. The reason I added a timeout is in case it can't connect. It's not that important since it's a coroutine, but if it times out, it's better handling it by notifying the user rather than trying over and over and loading forever.
The problem arises in your apiController.get() call, which returns immediately and not after the network operation is complete. You supply your response callback to it. It will run eventually, once the REST call has got its response.
This is how you should adapt your function to coroutines:
suspend fun getAdoptionList(): MutableList<AdoptionCard> {
adoptionCardList.takeIf { it.isNotEmpty() }?.also { return it }
suspendCancellableCoroutine<Unit> { cont ->
ApiController(GetVolley()).get("adoptions/read.php", JSONArray()) { response ->
// fill adoptionCardList from response
cont.resume(Unit)
}
}
return adoptionCardList
}
This is now a suspend fun and it will suspend itself in case the adoption list isn't already populated. In either case the function will ensure that by the time it returns, the list is populated.
I would also advise you to stop using GlobalScope in order to prevent your network calls running in the background, possibly holding on to the entire GUI tree of your activity, after the activity is destroyed. You can read more about structured concurrency from Roman Elizarov and you can follow the basic example in the documentation of CoroutineScope.

Categories

Resources