ARTICLE AD BOX
Expectation:
I added a Call Adapter to the application so that my network service wraps everything it returns in Result and maps all exceptions to my CustomException. It is surprising that everything works fine in debug mode.
Problem:
Even when I attach the OkHttp logger in the production build, I can see that the request returns a correct response (not null), but in the repository I receive null: authService.login(data).fold(onSuccess = { response -> // returns null } Is it possible to make this work with the system Result wrapper? Thanks for your help :)
My attempts to solve the problem:
I suppose it may be related to file cleaning by R8/ProGuard. I checked thoroughly whether the @keep annotation was added in the right places. I also tried using my own ApiResult wrapper instead of Result, which supposedly can be cleaned incorrectly by R8/ProGuard, but I had major difficulties implementing it.
Code
This is my custom call adapter:
@Keep @Suppress("UNCHECKED_CAST") class ResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) { return null } // get inner type val upperBound = getParameterUpperBound(0, returnType) return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) { // return new call adapter that change Call<Any> to ResultCall object : CallAdapter<Any, Call<Result<*>>> { override fun responseType(): Type = getParameterUpperBound(0, upperBound) override fun adapt(call: Call<Any>): Call<Result<*>> = ResultCall(call) as Call<Result<*>> } } else { null } } }Call implementation:
@Keep class ResultCall<T>(val delegate: Call<T>) : Call<Result<T>> { override fun enqueue(callback: Callback<Result<T>>) { delegate.enqueue( object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { // server response status code 200–299 if (response.isSuccessful) { Timber.d("response: ${response.body()}") callback.onResponse( this@ResultCall, Response.success( response.code(), Result.success(response.body()!!) ) ) } else { Timber.d("response has error") // server response status code 404, 500 val exception = when (response.code()) { 401, 403 -> CustomException.Unauthorized() 404 -> CustomException.NotFound() else -> { if (response.message().isEmpty()) CustomException.Unknown() else CustomException.Custom(response.message()) } } callback.onResponse( this@ResultCall, Response.success( Result.failure( exception ) ) ) } } // call with connection errors override fun onFailure(call: Call<T>, t: Throwable) { val exception = when (t) { is IOException -> CustomException.NoConnection() is HttpException -> CustomException.Unknown() else -> { if (t.localizedMessage.isNullOrEmpty()) CustomException.Unknown() else CustomException.Custom(t.localizedMessage!!) } } callback.onResponse( this@ResultCall, Response.success(Result.failure(exception)) ) } } ) } override fun execute(): Response<Result<T>> = Response.success(Result.success(delegate.execute().body()!!)) override fun isExecuted(): Boolean = delegate.isExecuted override fun cancel() = delegate.cancel() override fun isCanceled(): Boolean = delegate.isCanceled override fun clone(): Call<Result<T>> = ResultCall(delegate.clone()) override fun request(): Request = delegate.request() override fun timeout(): Timeout = delegate.timeout() }Network service: (I have added a keep annotation to LoginRequest and LoginResponse.)
@Keep interface AuthService { @POST("/Myapp/Login") suspend fun login(@Body body: LoginRequest) : Result<LoginResponse> // Here is the problem, it returns null instead of LoginResponse. @POST("/Myapp/Logout") suspend fun resetToken() : Result<Unit> @GET suspend fun resetSynodiPassword( @Url absoluteUrl: String, @Query("login") email: String ): Result<Boolean> }ProGuard file:
-keepattributes Signature -keepattributes *Annotation* -keepattributes InnerClasses -keepattributes EnclosingMethod -keep class kotlin.Metadata { *; } # remove logging -assumenosideeffects class android.util.Log { public static boolean isLoggable(java.lang.String, int); public static int v(...); public static int i(...); public static int w(...); public static int d(...); public static int e(...); } # Keep all fields for Gson reflection -keepclassmembers class * { @com.google.gson.annotations.SerializedName *; @com.google.gson.annotations.Expose *; } # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken