I saw many many stackoverflow about the same question but I still stuck in my circle.
I'm trying to return a user token from API. The response is success and I can LOG it. but I can't return it because the function will return before the enqueue completed (this is what I think)
Please see the code below:
fun login(username: String, password: String): Result<LoggedInUser> {
val client = OkHttpClient.Builder().build()
val retrofit = Retrofit.Builder()
.baseUrl("https:api/customers/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
val userApi= retrofit.create<UserApiKotlin>()
var pass: String ="SecYourPa$$"
val r= UserRequestModel("coding", pass)
var fakeUser = LoggedInUser(
java.util.UUID.randomUUID().toString(), "Jane Doe",
"gfgfgfgf", "trtrtr", "trtrtrtr", "abababababab"
)
var sss="1"
userApi.login(r).enqueue(object : Callback<LoggedInUser> {
override fun onResponse(
call: Call<LoggedInUser>, response: Response<LoggedInUser>
) {
fakeUser = LoggedInUser(
java.util.UUID.randomUUID().toString(), "Jane Doe",
"gfgfgfgf", "trtrtr", "trtrtrtr", "xcxcxcxc"
)
sss = "tttttt"
fakeUser = response.body()!!
Log.e("inside Retrofit", sss)
}
override fun onFailure(call: Call<LoggedInUser>, t: Throwable) {
Log.e("fauluer", "Unable to submit post to API.")
}
})
Log.e("outside retrofit", sss)
return Result.Success(fakeUser)
}
and this the output show that it will show and execute the function (and return the initial value) before api get the value from server
userApi.login(r).enqueue is an asynchronous call which will run on another thread. A network call can take severall milliseconds or seconds so you won't get the result immediately.
The code happens in this order:
You call userApi.login(r).enqueue -> Network call starts in parallel on another thread
You return Result.Success(fakeUser)
The network call finishes -> Code inside your callback in onResponse or onFailure is executed
As you see, you are returning a result before the network call even finishes. Your log shows the same.
Related
I am trying to make my recycler view wait until my retrofit function is done. The retrofit function is stored in a view model while the recyler view is in my activity. I can get it to work with a delay but that does not seem like a good way to do it.
GlobalScope.launch {
suspend {
val txt = findViewById<TextView>(R.id.textbox)
viewModel.getStaff()
delay(10000)
withContext(Dispatchers.Main) {
Log.d("coroutineScope", "#runs on ${Thread.currentThread().name}")
createRV(viewModel.pictureList, viewModel.actorList, viewModel.characterList, viewModel.houseList)
}
}.invoke()
and this is my retrofit function
suspend fun getStaff(){
val retrofit = Retrofit.Builder()
.baseUrl("http://hp-api.herokuapp.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(HarryPotterApi::class.java)
val call = service.staff()
try {
call.enqueue(object : Callback<Staff> {
override fun onResponse(call: Call<Staff>, response: Response<Staff>) {
if (response.code() == 200) {
val characterData = response.body()!!
pictureList.add(characterData[0].name)
actorList.add(characterData[0].name)
characterList.add(characterData[0].name)
houseList.add(characterData[0].name)
}
}
override fun onFailure(call: Call<Staff>, t: Throwable) {
}
})}catch (e: IOException) {
e.printStackTrace()
}
}
I suggest a better way.
Rather than delay (10000), hide the recyclerView and use a placeholder view instead
When you are ready to display the recyclerView:
withContext(Dispatchers.Main) {
placeholderView.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
Log.d("coroutineScope", "#runs on ${Thread.currentThread().name}")
createRV(viewModel.pictureList, viewModel.actorList, viewModel.characterList, viewModel.houseList)
}
Retrofit supports coroutines and suspend functions since 2.6.0, so you don't have to use callbacks and then somehow convert asynchronous call into suspendable (which is also possible). You can make HarryPotterApi.staff() itself suspendable and them make your whole code much simpler and synchronous, at the same time fixing your problem with waiting for getStaff() to finish.
I don't have your full code, but I assume HarryPotterApi.staff() is declared something like this:
fun staff(): Call<Staff>
Make it suspendable:
suspend fun staff(): Staff
Then use it synchronously:
suspend fun getStaff(){
val retrofit = Retrofit.Builder()
.baseUrl("http://hp-api.herokuapp.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(HarryPotterApi::class.java)
val characterData = service.staff()
pictureList.add(characterData[0].name)
actorList.add(characterData[0].name)
characterList.add(characterData[0].name)
houseList.add(characterData[0].name)
}
It may not be a fully working example, but I hope you get the idea.
Note that unlike your original solution, above service.staff() will throw an exception on the request failure (thanks #Tenfour04 for pointing this out). Depending on your needs you could catch it or propagate it to the outer code. Ignoring the failure, as in your original code, doesn't seem like a good solution, because the code that follows getStaff() call uses the data that getStaff() was supposed to retrieve.
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
}
}
}
}
I'm trying to releaze function for authorization my android app. I know there are many similar questions, but I'm interesting exectly to get function witch return "success" or "failure".
override fun authorization(login: String, password: String): String {
var result: String = "none"
val call: Call<UserAuthorizationDataModelRetrofit> =
dataBase.getWordApiRetrofit().userAuthorization(login, password)
call.enqueue(object : Callback<UserAuthorizationDataModelRetrofit> {
override fun onResponse(
call: Call<UserAuthorizationDataModelRetrofit>?,
response: Response<UserAuthorizationDataModelRetrofit>?
) {
val dataModel = response?.body()
result = dataModel?.result ?: "failure"
}
override fun onFailure(call: Call<UserAuthorizationDataModelRetrofit>?, t: Throwable?) {
result = "failure"
}
})
return result
}
I'm expecting function return "success" or "failure" acording to response of http request. But http request is asynchronous and every time I get "none". Can anybody help me?
The function .enqueue is asynchronous. You will always get a "none" because enqueue is always not finished when authorization returns.
You can do two things.
One is use enqueue and do all your work inside onResponse, open new Activity for the user, set your View or show a Toast, etc.
Another is use .execute. But you will have to call authorization in another Thread and do your work in there. Note that this is asynchronous and you should not do UI stuff here.
This function get data from API.
I don't understand its process.
Here the problem,
it returned weatherModel first and then it execute onResponse later.
It show in logcate Log.d("data", "Here") first
My function
fun get(city: String?): WeatherModel? {
var weatherModel: WeatherModel? = WeatherModel()
val retrofit =
retrofit2.Retrofit.Builder().baseUrl(Util.BASE_URL).addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(WeatherInterface::class.java)
val call = service.searchCity(city!!, "metric", "789af9673b393eb97f2acdea022f2005")
call.enqueue(object : Callback<WeatherModel> {
override fun onFailure(call: Call<WeatherModel>, t: Throwable) {
Log.d("data", "error ${t.message}")
}
override fun onResponse(call: Call<WeatherModel>, response: Response<WeatherModel>) {
Log.d("data", "success ${response.body()!!.weather!!.size}")
weatherModel = response.body()
data!!.getDataTrigger(weatherModel!!)
}
})
Log.d("data", "Here")
return weatherModel
}
So what is the problem?
It show in logcate Log.d("data", "Here") first
call.enqueue is a asynchronous call, which means it doesn't run on main thread, that's the reason you will get Log.d("data", "Here") first.
This is the reason you are getting null returned
If you are using this method asynchronously or in some service and you want to execute synchronously then kindly use call.execute but this will only work if you are using in other thread apart from Main thread as network operation are not allowed on main thread, in this case this method wont return null.
Either you should use live data or an interface for the callback to receive WeatherModel.
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.