I'm tring to get the response of a callback in sync mode because I the value of the response is needed to all application to work, without this value ( token ) I can't continue to use the application.
This is my companion object inside the retrofit interface. I need to set the token before creation of a client.
What I'm doing wrong?
EDIT :
I put as this Logs as you write :
companion object {
private var mToken: String = ""
fun create(activity: Activity): MyPhoneInterface {
Log.d("tokenMyPhoneInterface", activity.localClassName)
getToken(activity)
Log.d("tokenMyPhoneInterface", "client token $mToken")
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", mToken).build()
chain.proceed(request)
}.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.baseUrl(BuildConfig.API_HOST)
.build()
return retrofit.create(MyPhoneInterface::class.java)
}
private fun getToken(activity: Activity) {
if(!activity.isFinishing && isJwtExpired(mToken)){
val latch = CountDownLatch(1)
(LoginManager(activity)).getToken(true, object : ServiceCallback<String> {
override fun success(token: String) {
Log.d("tokenMyPhoneInterface", "token $token")
mToken = token
latch.countDown()
}
override fun failure(serviceError: ServiceError) {
Log.d("tokenMyPhoneInterface", serviceError.errorMessage)
latch.countDown()
}
})
Log.d("tokenMyPhoneInterface", "before await ")
latch.await()
Log.d("tokenMyPhoneInterface", "after await")
}
}
}
But I the system is blocked in the latch.await() and the logs is :
05-14 18:19:00.127 848-848/com.italy.myphone D/tokenMyPhoneInterface: view.splash.activity.Splash
05-14 18:19:00.131 848-848/com.italy.myphone D/tokenMyPhoneInterface: before await
EDIT answer2:
sealed class TokenResult {
class Success(val token: String) : TokenResult()
class Error(val serviceError: ServiceError) : TokenResult()}
suspend fun getToken(activity: Activity): TokenResult {
return suspendCancellableCoroutine { continuation ->
(LoginManager(activity)).getToken(true, object : ServiceCallback<String> {
override fun success(token: String) {
continuation.resume(TokenResult.Success(token))
}
override fun failure(serviceError: ServiceError) {
continuation.resume(TokenResult.Error(serviceError))
}
})
}}
And this is how to I try to call the suspend function :
companion object {
fun create(activity: Activity): MyPhoneInterface{
Log.d("tokenMyPhoneInterface", activity.localClassName)
var token = runBlocking {
return#runBlocking getToken(activity)
}
Log.d("tokenMyPhoneInterface", "obtained token")
Log.d("tokenMyPhoneInterface", "client token $tokenResult")
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "").build()
chain.proceed(request)
}.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.baseUrl(BuildConfig.API_HOST)
.build()
return retrofit.create(MyPhoneInterface::class.java)
}
}
That is inside an interface and this is the code that I use to call the interface/companion object in the activity :
private val mMyPhoneInterface by lazy {
MyPhoneInterface.create(activity)
}
The best way I know to get the response of a callback in sync mode is using
Coroutines and the function suspendCancellableCoroutine
In your case you can have this function:
suspend fun getToken(activity: Activity): TokenResult {
return suspendCancellableCoroutine { continuation ->
(LoginManager(activity)).getToken(true, object : ServiceCallback<String> {
override fun success(token: String) {
continuation.resume(TokenResult.Success(token))
}
override fun failure(serviceError: ServiceError) {
continuation.resume(TokenResult.Error(serviceError))
}
})
}
}
sealed class TokenResult {
class Success(val token: String) : TokenResult()
class Error(val serviceError: ServiceError) : TokenResult()
}
And in your activity.onCreate:
override fun onCreate(savedInstanceState: Bundle?) = runBlocking {
super.onCreate(savedInstanceState)
val tokenResult = getToken(this)
if(tokenResult is Error){
finish()
return#runBlocking
}
//create client here with tokenResult.token value
}
Give it a try and let me know...
EDIT: In the example I use runBlocking because getToken is a suspend function. In your own code you should handle this logic outside the activity.
EDIT:
To eneable coroutines in your project add the following lines in your gradle file:
dependencies {
//other dependencies
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"
}
kotlin {
experimental {
coroutines "enable"
}
}
At the end I use JavaRx in order to sync a callback.. this is the snippet.
fun getToken(loginManager: LoginManager): String {
return Single
.create(SingleOnSubscribe<String> { emitter ->
loginManager.getToken(object : TokenSimpleCallback {
override fun onSuccess(token: String) {
emitter.onSuccess(token)
}
override fun onFailure(loginServiceError: LoginServiceError) {
emitter.onError(Throwable(loginServiceError.toString()))
}
})
}).blockingGet()}
You shouldn't use latch.count. It is equals to 1 and latch.count > 1 is false. Then your function is returned. Just use latch.await() and it will await for one of callbacks.
Sorry if I am wrong, I don't have enough repo to comment.
Related
I want to show errors messages that come from my API response but every time onError() function is called inside RXJava It shows built-in Errors something Like time exception, network error, etc. please tell me how can I Show Api Response messages Like message.type , message.text these are my error messages. thanks
here is Api Response
{
"code": 400,
"status": "Bad Request",
"message": {
"type": "WARNING",
"text": "User was not found"
},
"data": {
"generatedMessage": false,
"code": "ERR_ASSERTION",
"actual": null,
"expected": true,
"operator": "=="
}
}
Here is api call
#POST("api/auth/login")
fun login(#Body loginModel: loginModel): Observable<LoginResponseModel>
Here is Base URl Code
private val CLIENT_URL = "http://rmms.dummy.org:3001"
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl(CLIENT_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build()
fun<T> buildService(service: Class<T>): T{
return retrofit.create(service)
}
Here is ViewModel
class LoginViewModel : ViewModel() {
var disposable = CompositeDisposable()
var serviceBuilder = ServiceBuilder.buildService(ApiServices::class.java)
var loginResponse: MutableLiveData<LoginResponseModel> = MutableLiveData()
fun refresh(requirContext: Context, email: String, password: String) {
login(requirContext, email, password)
}
fun login(requirContext: Context, email: String, password: String) {
var login = loginModel(email, password)
disposable.add(
serviceBuilder.login(login)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.unsubscribeOn(Schedulers.io())
.subscribeWith(object : DisposableSingleObserver<LoginResponseModel>(),
io.reactivex.Observer<LoginResponseModel> {
override fun onSuccess(value: LoginResponseModel?) {
}
override fun onError(e: Throwable?) {
if (e is retrofit2.HttpException) {
AlertMessageDialog.genaricAlertDialog(requirContext, "WARNING", "Invalid Credentials Provided"
)
} else if (e is SocketTimeoutException) {
Log.e("time exception******>", e.message.toString())
} else if (e is IOException) {
Log.e("network error******>", e.message.toString())
} else {
Log.e("unknown error******>", e?.message.toString())
}
}
override fun onNext(value: LoginResponseModel?) {
value?.let {
if (value.code == 200) {
}else{
AlertMessageDialog.genaricAlertDialog(requirContext,"${value.message.type}","${value.message.text}")
}
}
}
override fun onComplete() {
}
})
)
}
override fun onCleared() {
super.onCleared()
disposable.clear()
}
}
I am trying to Hit the API using the Retrofit Post method to put the form data but the problem is that the API returns null all the time even if I try to pass the params. I tested the API by running it in Postman it works fine and I also added internet permission and set usesCleartextTraffic to true under manifest.xml, so it would be really helpful if anyone guides me to rectify this issue.
Output When I tried to debugg
call = Unresolved reference: call
response = Unresolved reference: response
this = {MainActivity#9661}
params = {HashMap#10022} size = 5
"password" -> "Ram#123"
"countryCode" -> {Integer#10042} 91
"name" -> "Ranjith"
"mobile" -> {Long#10046} 99********
"emailID" -> "var***#gmail.com"
Response back from the API with body null all the time
response = {Response#10220} "Response{protocol=http/1.1, code=400, message=Bad Request, url=http://*.***.***.***:***0/api/v1/****/auth/register}"
body = null
errorBody = {ResponseBody$1#10224}
content = {Buffer#10228} "[text={"code":400,"error":true,"msg":"Enter a valid name"}]"
contentLength = 52
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
uploadBt.setOnClickListener {
callApi()
}
}
private fun callApi() {
val params = HashMap<String?, Any?>()
params["name"] = "Ranjith"
params["mobile"] = 99*******
params["emailID"] = "var***#gmail.com"
params["password"] = "Ram#123"
params["countryCode"] = 91
RetrofitClient.instance.registerUser(params)
.enqueue(object : Callback<DefaultResponse>{
override fun onResponse(call: Call<DefaultResponse>, response: Response<DefaultResponse>) {
print(response)
if(response.isSuccessful)
{
Toast.makeText(applicationContext, response.body()?.msg, Toast.LENGTH_SHORT).show()
}else
{
var jsonObject: JSONObject? = null
try {
jsonObject = JSONObject(response.errorBody()!!.string())
val code = jsonObject.getString("code")
val error = jsonObject.getString("error")
val message = jsonObject.getString("msg")
Toast.makeText(applicationContext, message+"\n"+code+"\n"+error, Toast.LENGTH_LONG).show()
} catch (e: JSONException) {
Toast.makeText(applicationContext, e.toString(), Toast.LENGTH_LONG).show()
e.printStackTrace()
}
}
}
override fun onFailure(call: Call<DefaultResponse>, t: Throwable) {
Toast.makeText(applicationContext, t.toString(), Toast.LENGTH_SHORT).show()
}
})
}
}
Interface Api
interface Api {
#FormUrlEncoded
#POST("/api/v1/******/****/register")
fun registerUser(
#FieldMap params: HashMap<String?, Any?>
): Call<DefaultResponse>
}
RetrofitClient.kt object class
object RetrofitClient {
private const val BASE_URL = "http://3.1**.**5.1*1:****"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
.method(original.method(), original.body())
val request = requestBuilder.build()
chain.proceed(request)
}.build()
val instance: Api by lazy{
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
retrofit.create(Api::class.java)
}
}
DefaultResponse.kt model data class
data class DefaultResponse(
#SerializedName("code")
val code: Int,
#SerializedName("err")
val error: Boolean,
#SerializedName("msg")
val msg: String
)
Through Postman
I am trying to create live data for the authToken in AccountManager.
This is how I am getting the auth token.
suspend fun Fragment.getAuthToken(): String? {
val am: AccountManager = AccountManager.get(activity)
val accounts: Array<out Account> = am.getAccountsByType(getAccountType())
var authToken: String? = null
if (accounts.isNotEmpty()) {
val account = accounts.first()
withContext(Dispatchers.IO) {
authToken = am.blockingGetAuthToken(account, getAccountType(), true)
}
}
return authToken
}
According to the documentation I should do something like this:
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
However I can't figure out how to convert the example to match my case.
According to another link
you can use live-data builder for coroutine:
val token: LiveData<String> = liveData {
val tokenValue = someYourFragment.getAuthToken()
emit(tokenValue)
}
I need to create a retrofit call adapter which can handle such network calls:
#GET("user")
suspend fun getUser(): MyResponseWrapper<User>
I want it to work with Kotlin Coroutines without using Deferred. I have already have a successful implementation using Deferred, which can handle methods such as:
#GET("user")
fun getUser(): Deferred<MyResponseWrapper<User>>
But I want the ability make the function a suspending function and remove the Deferred wrapper.
With suspending functions, Retrofit works as if there is a Call wrapper around the return type, so suspend fun getUser(): User is treated as fun getUser(): Call<User>
My Implementation
I have tried to create a call adapter which tries to handle this. Here is my implementation so far:
Factory
class MyWrapperAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
val rawType = getRawType(returnType)
if (rawType == Call::class.java) {
returnType as? ParameterizedType
?: throw IllegalStateException("$returnType must be parameterized")
val containerType = getParameterUpperBound(0, returnType)
if (getRawType(containerType) != MyWrapper::class.java) {
return null
}
containerType as? ParameterizedType
?: throw IllegalStateException("MyWrapper must be parameterized")
val successBodyType = getParameterUpperBound(0, containerType)
val errorBodyType = getParameterUpperBound(1, containerType)
val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(
null,
errorBodyType,
annotations
)
return MyWrapperAdapter<Any, Any>(successBodyType, errorBodyConverter)
}
return null
}
Adapter
class MyWrapperAdapter<T : Any>(
private val successBodyType: Type
) : CallAdapter<T, MyWrapper<T>> {
override fun adapt(call: Call<T>): MyWrapper<T> {
return try {
call.execute().toMyWrapper<T>()
} catch (e: IOException) {
e.toNetworkErrorWrapper()
}
}
override fun responseType(): Type = successBodyType
}
runBlocking {
val user: MyWrapper<User> = service.getUser()
}
Everything works as expected using this implementation, but just before the result of the network call is delivered to the user variable, I get the following error:
java.lang.ClassCastException: com.myproject.MyWrapper cannot be cast to retrofit2.Call
at retrofit2.HttpServiceMethod$SuspendForBody.adapt(HttpServiceMethod.java:185)
at retrofit2.HttpServiceMethod.invoke(HttpServiceMethod.java:132)
at retrofit2.Retrofit$1.invoke(Retrofit.java:149)
at com.sun.proxy.$Proxy6.getText(Unknown Source)
...
From Retrofit's source, here is the piece of code at HttpServiceMethod.java:185:
#Override protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call); // ERROR OCCURS HERE
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
}
I'm not sure how to handle this error. Is there a way to fix?
Here is a working example of an adapter, which automatically wraps a response to the Result wrapper. A GitHub sample is also available.
// build.gradle
...
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.google.code.gson:gson:2.8.5'
}
// test.kt
...
sealed class Result<out T> {
data class Success<T>(val data: T?) : Result<T>()
data class Failure(val statusCode: Int?) : Result<Nothing>()
object NetworkError : Result<Nothing>()
}
data class Bar(
#SerializedName("foo")
val foo: String
)
interface Service {
#GET("bar")
suspend fun getBar(): Result<Bar>
#GET("bars")
suspend fun getBars(): Result<List<Bar>>
}
abstract class CallDelegate<TIn, TOut>(
protected val proxy: Call<TIn>
) : Call<TOut> {
override fun execute(): Response<TOut> = throw NotImplementedError()
override final fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)
override final fun clone(): Call<TOut> = cloneImpl()
override fun cancel() = proxy.cancel()
override fun request(): Request = proxy.request()
override fun isExecuted() = proxy.isExecuted
override fun isCanceled() = proxy.isCanceled
abstract fun enqueueImpl(callback: Callback<TOut>)
abstract fun cloneImpl(): Call<TOut>
}
class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Result<T>>(proxy) {
override fun enqueueImpl(callback: Callback<Result<T>>) = proxy.enqueue(object: Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val code = response.code()
val result = if (code in 200 until 300) {
val body = response.body()
Result.Success(body)
} else {
Result.Failure(code)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val result = if (t is IOException) {
Result.NetworkError
} else {
Result.Failure(null)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
})
override fun cloneImpl() = ResultCall(proxy.clone())
}
class ResultAdapter(
private val type: Type
): CallAdapter<Type, Call<Result<Type>>> {
override fun responseType() = type
override fun adapt(call: Call<Type>): Call<Result<Type>> = ResultCall(call)
}
class MyCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
) = when (getRawType(returnType)) {
Call::class.java -> {
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
when (getRawType(callType)) {
Result::class.java -> {
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
ResultAdapter(resultType)
}
else -> null
}
}
else -> null
}
}
/**
* A Mock interceptor that returns a test data
*/
class MockInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val response = when (chain.request().url().encodedPath()) {
"/bar" -> """{"foo":"baz"}"""
"/bars" -> """[{"foo":"baz1"},{"foo":"baz2"}]"""
else -> throw Error("unknown request")
}
val mediaType = MediaType.parse("application/json")
val responseBody = ResponseBody.create(mediaType, response)
return okhttp3.Response.Builder()
.protocol(Protocol.HTTP_1_0)
.request(chain.request())
.code(200)
.message("")
.body(responseBody)
.build()
}
}
suspend fun test() {
val mockInterceptor = MockInterceptor()
val mockClient = OkHttpClient.Builder()
.addInterceptor(mockInterceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://mock.com/")
.client(mockClient)
.addCallAdapterFactory(MyCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(Service::class.java)
val bar = service.getBar()
val bars = service.getBars()
...
}
...
When you use Retrofit 2.6.0 with coroutines you don't need a wrapper anymore. It should look like below:
#GET("user")
suspend fun getUser(): User
You don't need MyResponseWrapper anymore, and when you call it, it should look like
runBlocking {
val user: User = service.getUser()
}
To get the retrofit Response you can do the following:
#GET("user")
suspend fun getUser(): Response<User>
You also don't need the MyWrapperAdapterFactory or the MyWrapperAdapter.
Hope this answered your question!
Edit
CommonsWare# has also mentioned this in the comments above
Edit
Handling error could be as follow:
sealed class ApiResponse<T> {
companion object {
fun <T> create(response: Response<T>): ApiResponse<T> {
return if(response.isSuccessful) {
val body = response.body()
// Empty body
if (body == null || response.code() == 204) {
ApiSuccessEmptyResponse()
} else {
ApiSuccessResponse(body)
}
} else {
val msg = response.errorBody()?.string()
val errorMessage = if(msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMessage ?: "Unknown error")
}
}
}
}
class ApiSuccessResponse<T>(val data: T): ApiResponse<T>()
class ApiSuccessEmptyResponse<T>: ApiResponse<T>()
class ApiErrorResponse<T>(val errorMessage: String): ApiResponse<T>()
Where you just need to call create with the response as ApiResponse.create(response) and it should return correct type. A more advanced scenario could be added here as well, by parsing the error if it is not just a plain string.
This question came up in the pull request where suspend was introduced to Retrofit.
matejdro: From what I see, this MR completely bypasses call adapters when using suspend functions. I'm currently using custom call adapters for centralising parsing of error body (and then throwing appropriate exceptions), smilarly to the official retrofit2 sample. Any chance we get alternative to this, some kind of adapter that is injected between here?
It turns out this is not supported (yet?).
Source: https://github.com/square/retrofit/pull/2886#issuecomment-438936312
For error handling I went for something like this to invoke api calls:
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>): MyWrapper<T> {
return try {
val response = call.invoke()
when (response.code()) {
// return MyWrapper based on response code
// MyWrapper is sealed class with subclasses Success and Failure
}
} catch (error: Throwable) {
Failure(error)
}
}
When registering in the application user gets 2 tokens. Access (lives 1 day) and Refresh (lives 6 months). At a certain point, the Access token will come-one day there will be a custom error. At this , we need to call the refreshToken method and the updated , with which the work will go on.
We call the method, for example getdata , checking for errors, if custom error refreshToken we keep both tokens getdata already with the updated token.
i try but how to rerty call method getdata after refresh token?
mAllApi.getData(new Request().getRequestData())
.flatMap(response -> {
if (response.getError().equals(ECode.ERROR_TOKEN.getCode())) {
mAllApi.getRefreshToken(new String()).flatMap(new Function<AccessToken, ObservableSource<AccessToken>>() {
#Override
public ObservableSource<AccessToken> apply(AccessToken accessToken) throws Exception {
AccessTokenManager.saveNewAccessToken(accessToken);
return null;
}
});
} else {
return Observable.just(response);
}
});
What we did in our app - we created custom OkHttp Interceptor which checks for Access Token each time we do Auth Request and if it's corrupted Interceptor change it with RefreshToken, Add new updated value to Authorization Header and retries Request.
Here is example in Kotlin:
class RefreshAccessTokenInterceptor
#Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(retryRequest(chain))
return if (response.noAuthError()) {
response
} else {
updateIfNeededOrProcessWithNewToken(chain)
}
}
private fun retryRequest(chain: Interceptor.Chain): Request {
val builder = chain.request().newBuilder()
addAuthHeaders(builder)
return builder.build()
}
private fun Response.noAuthError() = code() != HttpErrorChecker.HTTP_AUTHENTICATION_TIMEOUT
private fun addAuthHeaders(builder: Request.Builder) {
val accessToken = getAccessToken()
if (!accessToken.isNullOrEmpty()) {
builder.header("Authorization", "Bearer $accessToken")
}
}
private fun updateIfNeededOrProcessWithNewToken(chain: Interceptor.Chain): Response {
//here you update your token, add new header and retries request
return chain.proceed(retryRequest(chain))
}
}