I'm working in android studio, and am using kotlin coroutines to retrieve results from an API.
I need to wait til the coroutine is finished, so I can assign a global variable from it.
I've tested the URL and it's OK.
I've tried regular threads, which works, but am not able to make the main thread wait for it to finish.
I tried with Fuel.get() and it worked fine, but would like to use URL().
var response = "";
val req = "url.com"
runBlocking { launch {
response = URL(req).readText()
} }
Can anyone tell me why this code doesn't work? It throws an NetworkOnMainThreadException, but it's wrapped in a coroutine.
I fixed it. Ended up using a AsyncTask to read from URL, and a Handler to schedule handling the results.
var response = ""
#SuppressLint("StaticFieldLeak")
inner class Retriever : AsyncTask<String, String, String>() {
override fun doInBackground(vararg args : String?): String {
val urlRequest = args[0].toString()
var urlResponse = "";
//Try to extract url
try {
urlResponse = URL(urlRequest).readText()
println("SUCCESS in Retrieve.")
} catch (e : Exception) {
println("EXCEPTION in Retrieve.")
e.printStackTrace()
}
return urlResponse;
}
//Assigns value to response
override fun onPostExecute(result: String?) {
response = result.toString() //Result possibly void type
}
}
override fun onCreate() {
Retriever().execute("url.com")
Handler().{/*Handle response here*/, 10000)
}
Try this way
var response = "";
val req = "url.com"
runBlocking<Unit> {
GlobalScope.launch {
response = URL(req).readText()
}
//Work with the response here
}
You can preview all coroutine documentation here
If you just want handle the result of URL(req).readText() in another thread,do as following codes.
var response = "";
val req = "url.com"
runBlocking<Unit> {
GlobalScope.launch {
response = URL(req).readText()
//here is another thread,handle response here
}
//here is main thread, you can't get the result of URL(req).readText() because io operation need a long time .
}
If you handle the result in the main thread , please use Hanlder class
For me the below code works smoothly.
var response = "";
val req = "yoururl.com"
runBlocking {
try {
withTimeout(5000) { // 5 seconds for timeout
launch(Dispatchers.IO) { // using IO Dispatcher and not the default
response = URL(req).readText()
} // launch
} // timeout
} catch (e:Exception) { // Timeout, permission, URL or network error
response="Error" // Here one uses a not valid message
}
// Here one manages 'response' variable for error handling or valid output.
It's also important add the permission in AndroidManifest.xml, inside main manifest tag:
<uses-permission android:name="android.permission.INTERNET"/>
An advantage of this type of approach (for tasks that are not too long ) is simplicity because it does not require the use of a callback routine, since it is a sequential code.
Related
In all cases that I have been using corrutines, so far, it has been executing its "lines" synchronously, so that I have been able to use the result of a variable in the next line of code.
I have the ImageRepository class that calls the server, gets a list of images, and once obtained, creates a json with the images and related information.
class ImageRepository {
val API_IMAGES = "https://api.MY_API_IMAGES"
suspend fun fetch (activity: AppCompatActivity) {
activity.lifecycleScope.launch() {
val imagesResponse = withContext(Dispatchers.IO) {
getRequest(API_IMAGES)
}
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
...// All the rest of code
}
}
}
Well, the suspend function executes correctly synchronously, it first makes the call to the server in the getRequest and, when there is response, then composes the JSON. So far, so good.
And this is the call to the "ImageRepository" suspension function from my main activity:
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { neoRepository.fetch(this#MainActivity) }
Log.i(TAG, "After suspend fun")
}
The problem is that, as soon as it is executed, it calls the suspension function and then displays the log, obviously empty. It doesn't wait for the suspension function to finish and then display the log.
Why? What am I doing wrong?
I have tried the different Dispatchers, etc, but without success.
I appreciate any help.
Thanks and best regards.
It’s because you are launching another coroutine in parallel from inside your suspend function. Instead of launching another coroutine there, call the contents of that launch directly in your suspend function.
A suspend function is just like a regular function, it executes one instruction after another. The only difference is that it can be suspended, meaning the runtime environment can decide to halt / suspend execution to do other work and then resume execution later.
This is true unless you start an asynchronous operation which you should not be doing. Your fetch operation should look like:
class ImageRepository {
suspend fun fetch () {
val imagesResponse = getRequest(API_IMAGES)
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
... // All the rest of code
}
}
-> just like a regular function. Of course you need to all it from a coroutine:
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { neoRepository.fetch() }
Log.i(TAG, "After suspend fun")
}
Google recommends to inject the dispatcher into the lower level classes (https://developer.android.com/kotlin/coroutines/coroutines-best-practices) so ideally you'd do:
val neoRepository = ImageRepository(Dispatchers.IO)
lifecycleScope.launch {
val result = neoRepository.fetch()
Log.i(TAG, "After suspend fun")
}
class ImageRepository(private val dispatcher: Dispatcher) {
suspend fun fetch () = withContext(dispatcher) {
val imagesResponse = getRequest(API_IMAGES)
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
... // All the rest of code
}
}
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
}
}
}
}
Since AsyncTask is deprecated from Android R, i cannot parse or accept a JSON Data from the webhttp://plantplaces.com/perl/mobile/flashcard.pl. So what i did is. I created a kotlin class named DownloadingObject. Below is the code
class DownloadingObject {
#Throws(IOException::class)
fun downloadJSONDataFromLink(link: String): String {
val stringBuilder: StringBuilder = StringBuilder()
val url: URL = URL(link)
val urlConnection = url.openConnection() as HttpURLConnection
try {
val bufferedInputString: BufferedInputStream =
BufferedInputStream(urlConnection.inputStream)
val bufferedReader: BufferedReader =
BufferedReader(InputStreamReader(bufferedInputString))
// temporary string to hold each line read from the BufferedReader.
var inputLineString: String?
inputLineString = bufferedReader.readLine()
while (inputLineString != null) {
stringBuilder.append(inputLineString)
inputLineString = bufferedReader.readLine()
}
} finally {
// regardless of success of Try Block or failure of Try Block, we will disconnect from the URLConnection.
urlConnection.disconnect()
}
return stringBuilder.toString()
}
And i have to run this downloading task in the background. Below is the code
inner class DownloadingPlantTask : AsyncTask>() {
override fun doInBackground(vararg params: String?): List<Plant>? {
// Can access background thread. Not user interface thread
val downloadingObject: DownloadingObject = DownloadingObject()
var jsonData = downloadingObject.downloadJSONDataFromLink(
"http://plantplaces.com/perl/mobile/flashcard.pl"
)
Log.i("JSON", jsonData)
return null
}
Please if anyone can provide any alternative codes. I am just a bieginner in Android development.
There are several ways to achieve multithreading on Android. Since you're using Kotlin you may want to look into Kotlin Coroutines. Some other things out there are the java.concurrent.* package and RxJava, but in my opinion Coroutines are more beginner-friendly and they integrate with Android components very well.
With coroutines, your code would look like this:
#Throws(IOException::class)
suspend fun downloadJSONDataFromLink(link: String): String { ... }
GlobalScope.launch {
val downloadingObject = DownloadingObject()
val jsonData = downloadingObject.downloadJSONDataFromLink("http://plantplaces.com/perl/mobile/flashcard.pl")
Log.i("JSON", jsonData)
}
The suspend keyword means this function can only be called from a coroutine or from another suspending function.
Also you should consider using your Activity/Fragment's lifecycle scope instead of GlobalScope to avoid memory leaks.
I know that an AsyncTask can be run only once. I know a way around that, but I need a variable from the AsyncTask that uses complicated(?) processes. This is my code for calling the AsyncTask
val thr=NewTask()
thr.delegate = this
button.setOnClickListener {
thr.execute()
}
NewTask.doOnBackground() is just a normal method sending the request to the URL. onPostExecute() is a bit different:
public override fun onPostExecute(result: String?) {
//super.onPostExecute(result)
delegate!!.processFinish(result!!)
}
with delegate being a variable of AsyncResponse? which is an interface containing processFinish abstract method taking a string and returning nothing.
My question is, how can I run the AsyncTask repeatedly while still getting the response? Thanks in advance.
Finally, I settled on using coroutines with this. Coroutines are easy to use, much easier than AsyncTask. I don't know why I was scared of them. Here is the code I used:
class CoRoutine{
suspend fun httpGet(url: String = "https://boogle.org"): String {
val arr = ArrayList<String>()
withContext(Dispatchers.IO) {
val url = URL(url)
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET" // optional default is GET
//arr.add(responseCode)
inputStream.bufferedReader().use {
it.lines().forEach { line ->
//println(line)
arr.add(line as String)
}
}
}
}
return arr.get(0)
}
}
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.