How can I return a value after a callback in kotlin, I tried using Thread.sleep but it doesn't work
fun searchColorFromAPI(): Colors {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
if (serviceResponse != null) {
mColors = serviceResponse
}
else {
//buildToast(getString(R.string.null_response))
}
}
else {
//buildToast(getString(R.string.response_unsuccessful))
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
/* buildToast(getString(R.string.error_calling_service))
Log.e(TAG, t?.message)*/
}
})
return mColors
}
Always, the mColors is returned before the onFailure or onResponse because they're asynchronous. Before this code was in MainActivity but I was advised to take off, but now when I try get mColors I get the empty value before and after the onResponse is executed, please I'm still learning Kotlin and Android.
Your problem stems from the fact that Retrofit call is asynchronous, so as soon as you call searchColorFromAPI it returns you mColors but the API call may not have been made yet, so you get the mColors value before API call.
To solve this issue, you can do
Use callback, this will require little modification in your current setup, but the 2nd option is preferable over this. Using callback your function should look like this.
/* Now instead of returning a value, your function takes a function (named callback)
as parameter. when your api call finishes, you can call the callback function and
pass the api response.
*/
fun searchColorFromAPI(callback: (Colors?) -> Unit) {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
/** pass API response to callback */
callback(serviceResponse)
}
else {
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
callback(null)
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
callback(null)
}
})
}
And in your activity declare a function as follows.
// This function will be called when your api call finishes
// and it will give you the api response
fun apiCallback(colors: Colors?){
if(colors == null){
// API Call failed
}
else{
// use colors as returned by API
}
}
And now call to searchColorFromApi should look like this
searchColorFromApi(apiCallback)
Use Live Data, declare following field in your viewmodel, if you are not using viewmodel then declare it in the class which has searchColorFromApi function.
var colors: MutableLiveData<Colors> = MutableLiveData()
and modify your searchColorFromAPI function as follows
fun searchColorFromAPI() {
val service: RetrofitService = ServiceGenerator.createService(RetrofitService::class.java)
val result: MutableList<String> = arrayListOf()
val call: Call<Colors?>? = service.unityConverter(result)
call?.enqueue(object : Callback<Colors?> {
override fun onResponse(call: Call<Colors?>?, response: Response<Colors?>) {
//switchProgressVisibility()
if (response.isSuccessful) {
val serviceResponse: Colors? = response.body()
if (serviceResponse != null) {
colors.postValue(response.body)
}
}
else {
colors.postValue(null)
val errorBody: ResponseBody = response.errorBody()
Log.e(TAG, errorBody.toString())
}
}
override fun onFailure(call: Call<Colors?>?, t: Throwable?) {
colors.postValue(null)
}
})
}
and in your activity do following
fun setupObservers(){
yourApiCallingClass.colors.observe(this, Observer {
// this code is called when ever value of color field changes
})
}
You can use live data ,that gets updated once the callback receives ,the same live data is observed by the caller fragment/activity
You can use coroutines to return a value from function which has asyn calls in it.
You can use interface callbacks to activity/ fragment to trigger the updates received from retrofit calls.
Related
I'm new to kotlin coroutines. I've been trying to run multiple API calls in parallel and then when all the calls are done update my UI and dismiss the loader, but with no success. This is my code
private fun getScoreForType() {
val job = CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
}
runBlocking {
job.join()
// do some ui work
dismissLoader()
}
}
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
I've also tried using async and awaitAll but couldn't make it work either. The loader is always dismissed before all the calls are done. Any help on how I could make this work would be much appreciated
Use Flow and collectData it will works as LiveData.
For example:
val myIntFlow = MutableStateFlow(-1)
Try something like;
in ViewModelMethods
private fun getScoreForType() {
It goes first:
CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
// it means to change value of flow
myIntFlow.value = 1
}
// Now collect data in fragment to change UI
}
// in fragment like:
CoroutineScope(Dispatchers.Main).launch {
// flow will be triggered, on every changed value
viewModel.myIntFlow.collect {
viewModel.methodFromViewModelToChangeUI()
dissmisloader()
myIntFlow.value = -1
}
}
// try the same here as you wish
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
The question about post requests in android has been asked before, but all the solutions I've tried have not worked properly. On top of that, a lot of them seem to be overly complicated as well. All I wish to do is make a post to a specific sight with a few body parameters. Is there any simple way to do that?
Let me explain my request calling structure using Retrofit.
build.gradle(app)
// Retrofit + GSON
implementation 'com.squareup.okhttp3:logging-interceptor:4.4.0'
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
ApiClient.kt
object ApiClient {
private const val baseUrl = ApiInterface.BASE_URL
private var retrofit: Retrofit? = null
private val dispatcher = Dispatcher()
fun getClient(): Retrofit? {
val logging = HttpLoggingInterceptor()
if (BuildConfig.DEBUG)
logging.level = HttpLoggingInterceptor.Level.BODY
else
logging.level = HttpLoggingInterceptor.Level.NONE
if (retrofit == null) {
retrofit = Retrofit.Builder()
.client(OkHttpClient().newBuilder().readTimeout(120, TimeUnit.SECONDS)
.connectTimeout(120, TimeUnit.SECONDS).retryOnConnectionFailure(false)
.dispatcher(
dispatcher
).addInterceptor(Interceptor { chain: Interceptor.Chain? ->
val newRequest = chain?.request()!!.newBuilder()
return#Interceptor chain.proceed(newRequest.build())
}).addInterceptor(logging).build()
)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
ApiClient will be used to initialize Retrofit singleton object, also initialize logging interceptors so you can keep track of the requests and responses in the logcat by using the keyword 'okhttp'.
SingleEnqueueCall.kt
object SingleEnqueueCall {
var retryCount = 0
lateinit var snackbar: Snackbar
fun <T> callRetrofit(
activity: Activity,
call: Call<T>,
apiName: String,
isLoaderShown: Boolean,
apiListener: IGenericCallBack
) {
snackbar = Snackbar.make(
activity.findViewById(android.R.id.content),
Constants.CONST_NO_INTERNET_CONNECTION, Snackbar.LENGTH_INDEFINITE
)
if (isLoaderShown)
activity.showAppLoader()
snackbar.dismiss()
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
hideAppLoader()
if (response.isSuccessful) {
retryCount = 0
apiListener.success(apiName, response.body())
} else {
when {
response.errorBody() != null -> try {
val json = JSONObject(response.errorBody()!!.string())
Log.e("TEGD", "JSON==> " + response.errorBody())
Log.e("TEGD", "Response Code==> " + response.code())
val error = json.get("message") as String
apiListener.failure(apiName, error)
} catch (e: Exception) {
e.printStackTrace()
Log.e("TGED", "JSON==> " + e.message)
Log.e("TGED", "Response Code==> " + response.code())
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
}
else -> {
apiListener.failure(apiName, Constants.CONST_SERVER_NOT_RESPONDING)
return
}
}
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
hideAppLoader()
val callBack = this
if (t.message != "Canceled") {
Log.e("TGED", "Fail==> " + t.localizedMessage)
if (t is UnknownHostException || t is IOException) {
snackbar.setAction("Retry") {
snackbar.dismiss()
enqueueWithRetry(activity, call, callBack, isLoaderShown)
}
snackbar.show()
apiListener.failure(apiName, Constants.CONST_NO_INTERNET_CONNECTION)
} else {
retryCount = 0
apiListener.failure(apiName, t.toString())
}
} else {
retryCount = 0
}
}
})
}
fun <T> enqueueWithRetry(
activity: Activity,
call: Call<T>,
callback: Callback<T>,
isLoaderShown: Boolean
) {
activity.showAppLoader()
call.clone().enqueue(callback)
}
}
SingleEnqueueCall will be used for calling the retrofit, it is quite versatile, written with onFailure() functions and by passing Call to it, we can call an API along with ApiName parameter so this function can be used for any possible calls and by ApiName, we can distinguish in the response that which API the result came from.
Constants.kt
object Constants {
const val CONST_NO_INTERNET_CONNECTION = "Please check your internet
connection"
const val CONST_SERVER_NOT_RESPONDING = "Server not responding!
Please try again later"
const val USER_REGISTER = "/api/User/register"
}
ApiInterface.kt
interface ApiInterface {
companion object {
const val BASE_URL = "URL_LINK"
}
#POST(Constants.USER_REGISTER)
fun userRegister(#Body userRegisterRequest: UserRegisterRequest):
Call<UserRegisterResponse>
}
UserRegisterRequest.kt
data class UserRegisterRequest(
val Email: String,
val Password: String
)
UserRegisterResponse.kt
data class UserRegisterResponse(
val Message: String,
val Code: Int
)
IGenericCallBack.kt
interface IGenericCallBack {
fun success(apiName: String, response: Any?)
fun failure(apiName: String, message: String?)
}
MyApplication.kt
class MyApplication : Application() {
companion object {
lateinit var apiService: ApiInterface
}
override fun onCreate() {
super.onCreate()
apiService = ApiClient.getClient()!!.create(ApiInterface::class.java)
}
}
MyApplication is the application class to initialize Retrofit at the launch of the app.
AndroidManifest.xml
android:name=".MyApplication"
You have to write above tag in AndroidManifest inside Application tag.
MainActivity.kt
class MainActivity : AppCompatActivity(), IGenericCallBack {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val call = MyApplication.apiService.userRegister(UserRegisterRequest(email, password))
SingleEnqueueCall.callRetrofit(this, call, Constants.USER_REGISTER, true, this)
}
override fun success(apiName: String, response: Any?) {
val model = response as UserRegisterResponse
}
override fun failure(apiName: String, message: String?) {
if (message != null) {
showToastMessage(message)
}
}
}
Firstly, we create a call object by using the API defined in ApiInterface and passing the parameters (if any). Then using SingleEnqueueCall, we pass the call to the retrofit along with ApiName and the interface listener IGenericCallBack by using this. Remember to implement it to respective activity or fragment as above.
Secondly, you will have the response of the API whether in success() or failure() function overriden by IGenericCallBack
P.S: You can differentiate which API got the response by using the ApiName parameter inside success() function.
override fun success(apiName: String, response: Any?) {
when(ApiName) {
Constants.USER_REGISTER -> {
val model = response as UserRegisterResponse
}
}
}
The whole concept is to focus on reusability, now every API call has to create a call variable by using the API's inside ApiInterface then call that API by SingleEnqueueCall and get the response inside success() or failure() functions.
I am new android developer, how can I get result form this snippet, what way does exist, because it doesn't return anything, because of I'm adding element inside onResponse, but using only kotlin module:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
}
})
return list
}
}
You can give your function a callback parameter that's called when the response is receieved. And you shouldn't have an input list in this case, because if you have multiple sources modifying it at unpredictable future moments, it will be difficult to track.
The function can look like this:
private fun getCurrencyModels(callback: (ArrayList<CurrencyModel>) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
val list = arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
callback(list)
}
})
}
And then to use it:
getCurrencyModels { modelsList ->
// do something with modelsList when it arrives
}
An alternative is to use coroutines, which allow you to do asynchronous actions without callbacks. Someone has already created a library that lets you use OkHttp requests in coroutines here. You could write your function as a suspend function like this:
private suspend fun getCurrencyModels(): ArrayList<CurrencyModel> {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
val response = client.newCall(request).await()
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
return arrayListOf(
CurrencyModel("USD", data.rates.USD, 0.0)),
CurrencyModel("SEK", data.rates.SEK, 0.0)),
CurrencyModel("EUR", data.rates.EUR, 0.0))
)
}
and then use it like this:
lifecycleScope.launch {
try {
val currencyModels = getCurrencyModels()
// do something with currencyModels
} catch (e: IOException) {
// request failed
}
}
Coroutines make it really easy to avoid leaking memory when your asynchronous calls outlive your Activity or Fragment. In this case, if your Activity closes while the request is going, it will be cancelled automatically and references to your Activity will be removed so the garbage collector can release your Activity.
The onResponse() function is only called when the HTTP response is successfully returned by the remote server. Since this response doesn't happen immediately, you can't use the result in your code immediately. What you could do is use a ViewModel and LiveData variable and add the values to that variable in onResponse(). Something like:
private fun foo(list: ArrayList<CurrencyModel> = ArrayList()) {
val request = Request.Builder().url(BASE_URL_YESTERDAY).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
val data = Gson().fromJson(body, Currencies::class.java)
val list: ArrayList<CurrencyModel> = arrayListOf()
list.add(CurrencyModel("USD", data.rates.USD, 0.0))
list.add(CurrencyModel("SEK", data.rates.SEK, 0.0))
list.add(CurrencyModel("EUR", data.rates.EUR, 0.0))
viewModel.list.postValue(list)
}
})
}
Here is my rest controller method
fun getPosts() : ArrayList {
val result = ArrayList<Post>()
val retrofit = RetrofitClient.retrofitInstance
val service = retrofit?.create(ApiService::class.java)
val call = service?.getPosts()
call?.enqueue(object : Callback<List<Post>> {
override fun onFailure(call: Call<List<Post>>, t: Throwable) {
}
override fun onResponse(call: Call<List<Post>>, response: Response<List<Post>>) {
println(response.body()?.size)
if(response.isSuccessful && response.body()!= null) {
val posts = response.body()
listener.onRestDataReceived(posts!!)
result.addAll(posts!!)
}
}
})
return result
}
When I try to access the data from return type I get the empty array list but if I use a interface callback method and pass the data through the interface and implement the interface in my activity I can access the data
The enqueue(Callback<T> callback) method your are calling is asynchronous, so the actual result of enqueue(Callback<T> callback) you get in callback methods. In your getPosts() method after calling call?.enqueue your code execution immediately passes to return result; statement and that's why your are getting an empty array.
I am changing a global variable value inside a method and trying to return it later .
In FetchData.kt ( The called class )
var homeFeed: HomeFeed? = null // the variable that needs to be changed
fun execute() {
val client = OkHttpClient()
val url =
"..."
val request = Request.Builder().url(url).build()
val res = client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
val ch = response?.body?.string()
val gson = GsonBuilder().create()
homeFeed= gson.fromJson(ch, HomeFeed::class.java) // where the change happens
}
})
}
fun GetData(): HomeFeed? {
return homeFeed
}
In MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn.setOnClickListener {
val destination = "destination"
val places = FetchData(destination)
places.execute()
val data = places.GetData()
}
}
The problem is that a Null value is assigned to "data" variable in MainActivity as if the returned "homeFeed" variable wasn't changed at all .
I debugged the code to get ensured that no errors occur with api call and the variable is changed inside the method ( but not outside it! )
I am really stuck with that , any help ?
You are calling getData() immediately after making the async request, so it hasn't had a chance to be updated. Asynchronous functions do not immediately return. Some background thread does something (a network request), and in this case calls onResponse when the result is returned some time in the future.
It's just like the listener on your button. The code in the listener isn't run immediately, but some time in the future when the user presses it.
Instead of using this member property, your function can take a callback parameter that it invokes when the result is ready:
fun execute(resultHandler: (HomeFeed) -> Unit) {
//... snip ...
client.newCall(request).enqueue(object : Callback {
// ... snip ...
override fun onResponse(call: Call, response: Response) {
val ch = response?.body?.string()
val gson = GsonBuilder().create()
resultHandler(gson.fromJson(ch, HomeFeed::class.java))
}
})
}
Then when you call it, you pass a lambda that will be called when the result is ready:
btn.setOnClickListener {
val destination = "destination"
val places = FetchData(destination)
places.execute { homeFeedData ->
// Do something with homeFeedData when it arrives
}
}