I have trouble with a Retrofit call. I have a node js server which is working fine and I want to send a login request to it from Android.
The HTTP request expects a JSON request body. I try to create it with OkHttp3.
Here is my interface code:
interface ApiService {
#POST("/login-player")
suspend fun loginPlayer(#Body body: RequestBody): Call<PlayerApiModel>
}
Of course, I have more paths than this but first of all, I want this to work.
My model class, if anyone interested:
data class PlayerApiModel(
#SerializedName("player_id") #Expose var playerId: Int,
#SerializedName("_id") #Expose var id: String,
#SerializedName("player_name") #Expose var name: String,
#SerializedName("password_hash") #Expose var password: String,
#SerializedName("_v") #Expose var version: Int
)
This is exactly the same schema as I get from the node server. But the problem is the request cannot even reach the server.
My Retrofit singleton looks like this:
class RetrofitInstance {
companion object {
private const val URL = "https://pedro.sch.bme.hu/"
val retrofit: ApiService by lazy {
val interceptor = HttpLoggingInterceptor()
val httpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor { chain ->
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", UUID.randomUUID().toString())
.build()
chain.proceed(newRequest)
}
val builder = Retrofit.Builder()
.baseUrl(URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addCallAdapterFactory(SimpleCallAdapterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
val retrofit = builder
.client(httpClient.build())
.build()
retrofit.create(ApiService::class.java)
}
}
}
And lastly here is the problematic code part: the call.
val playerName = bind.root.txtPlayerName.text.toString()
val password = bind.root.txtPassword.text.toString()
val jsonObject = JSONObject()
jsonObject.put("player_name", playerName)
jsonObject.put("password", password)
val jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonObject.toString())
RetrofitInstance.retrofit.loginPlayer(jsonBody)
.enqueue(object : Callback<PlayerApiModel> {
override fun onResponse(call: Call<PlayerApiModel>, response: Response<PlayerApiModel>) {
println("${response.code()}: ${response.message()}")
when (response.code()) {
200 -> {
listener.goToMenu(playerName)
}
400 -> {
enableEditTexts()
Snackbar.make(bind.root, response.message(), Snackbar.LENGTH_LONG).show()
}
500 -> {
enableEditTexts()
serverErrorSnackbar()
}
}
}
override fun onFailure(call: Call<PlayerApiModel>, t: Throwable) {
Log.i("LoginViewModel::login()", t.message)
enableEditTexts()
}
})
The exception that I get comes to the loginPlayer() call.
This is the complete stack trace:
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: neptun.jxy1vz.cluedo, PID: 14287
java.lang.IllegalArgumentException: Unable to create call adapter for class java.lang.Object
for method ApiService.loginPlayer
at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:755)
at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:240)
at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:165)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy1.loginPlayer(Unknown Source)
at neptun.jxy1vz.cluedo.ui.activity.login.LoginViewModel$login$1.invokeSuspend(LoginViewModel.kt:57)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
Tried:
* retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
* neptun.jxy1vz.cluedo.network.call_adapter.SimpleCallAdapterFactory
* retrofit2.ExecutorCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:238)
... 12 more
I/Process: Sending signal. PID: 14287 SIG: 9
Update:
I got a suggestion to remove RxJava2CallAdapterFactory from Retrofit.
Now the bottom of the stack trace is this:
Tried:
* neptun.jxy1vz.cluedo.network.call_adapter.SimpleCallAdapterFactory
* retrofit2.ExecutorCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:238)
... 12 more
You can see here my SimpleCallAdapter, which is pretty simple (haha), I wrote this according to a tutorial, but I did not do any special thing in it:
class SimpleCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(returnType: Type?, annotations: Array<out Annotation>?, retrofit: Retrofit?): CallAdapter<*, *>? =
returnType?.let {
return try {
// get enclosing type
val enclosingType = (it as ParameterizedType)
// ensure enclosing type is 'Simple'
if (enclosingType.rawType != Simple::class.java)
null
else {
val type = enclosingType.actualTypeArguments[0]
SimpleCallAdapter<Any>(type)
}
} catch (ex: ClassCastException) {
null
} }
companion object {
#JvmStatic
fun create() = SimpleCallAdapterFactory()
}
}
class SimpleCallAdapter<R>(private val responseType: Type): CallAdapter<R, Any> {
override fun responseType(): Type = responseType
override fun adapt(call: Call<R>): Any = Simple(call)
}
class Simple<R>(private val call: Call<R>) {
fun run(responseHandler: (R?, Throwable?) -> Unit) {
// run in the same thread
try {
// call and handle response
val response = call.execute()
handleResponse(response, responseHandler)
} catch (t: IOException) {
responseHandler(null, t)
}
}
fun process(responseHandler: (R?, Throwable?) -> Unit) {
// define callback
val callback = object : Callback<R> {
override fun onFailure(call: Call<R>?, t: Throwable?) =
responseHandler(null, t)
override fun onResponse(call: Call<R>?, r: Response<R>?) =
handleResponse(r, responseHandler)
}
// enqueue network call
call.enqueue(callback)
}
private fun handleResponse(response: Response<R>?, handler: (R?, Throwable?) -> Unit) {
response?.let {
if (response.isSuccessful) {
handler(response.body(), null)
println(response.body())
}
else {
println("${response.code()}: ${response.message()}")
if (response.code() in 400..511)
handler(null, HttpException(response))
else
handler(response.body(), null)
}
}
}
}
And the usage of this:
RetrofitInstance.retrofit.loginPlayer(jsonBody).process { playerApiModel, throwable ->
println("Debug: ${playerApiModel?.name}")
}
And in the interface:
interface ApiService {
#POST("/login-player")
suspend fun loginPlayer(#Body body: RequestBody): Simple<PlayerApiModel>
}
End of update
Curious that it does not recognize the RequestBody type and instead it tries to pass java.lang.Object.
I have all the necessary dependencies in Gradle.
Related
I'm new to retrofit, and attempting to set a converter that will automatically deserialize error messages for result.errorBody
CustomErrorHandler
class CustomErrorHandler(private val gson: Gson) : Converter<ResponseBody, Error> {
#Throws(IOException::class)
override fun convert(responseBody: ResponseBody): Error {
val error = gson.fromJson(responseBody.charStream(), Error::class.java)
responseBody.close()
throw Exception(error.message)
}
class Factory(private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
val typeToken = TypeToken.get(type)
if (typeToken.rawType != Error::class.java) {
return null
}
return CustomErrorHandler(gson)
}
}
}
Retrofit
val retrofit = Retrofit.Builder().baseUrl("http://192.168.0.1:8080")
.addConverterFactory(CustomErrorHandler.Factory(GsonBuilder().create()))
.addConverterFactory(GsonConverterFactory.create()).build()
Error
data class Error(val code: String, val message: String) {
override fun toString(): String {
return "code: $code, message: $message"
}
}
RoleService
interface RoleService {
#GET("/roles")
#Headers("Accept: application/json")
suspend fun findAll(): Response<List<Role>>
}
Attempt #1
expecting that following throw statement will have result.errorBody as the Error class or please advise on how it's supposed to be done.
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
throw Exception(result.errorBody().toString()) // result.errorBody to be Error class???
}
Attempt #2
A simple approach without using any converter and deserializing errorBody to Error class
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
val e = Gson().fromJson(result.errorBody().toString(), Error::class.java) // fails too
throw Exception(e.message)
}
This inline solution helped, I couldn't get Converter working.
throw Exception(gson.fromJson(result.errorBody()?.string(), Error::class.java)?.message)
complete code
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
throw Exception(gson.fromJson(result.errorBody()?.string(), Error::class.java)?.message)
}
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.
My problem is, when I use Call inside my retrofit api, I get this exception:
Unable to create converter for retrofit2.Call<java.util.List>
for method IntegApi.getAllProductBarcodeAsync
The intersting thing, if I don't use Call then the error message is gone.
I want to use Call, becuse I would like to use a custom response class, because i want the know when the api sends 404 status, and then I want to skip this exception.
I use Moshi to convert Json
Sync function:
private suspend fun syncProductBarcodes() {
try {
val productBarcodes = api.getAllProductBarcodeAsync().await()
if(productBarcodes.isSuccessful) {
productBarcodeRepository.deleteAndInsertAll(productBarcodes.body() ?: emptyList())
addOneToStep()
}
}catch (e: Exception){
Timber.d(e)
throw e
}
}
Api:
#GET("Product/GetAllBarcode")
suspend fun getAllProductBarcodeAsync(): Call<List<ProductBarcode>>
Entity class:
#Entity(
tableName = ProductBarcode.TABLE_NAME
)
#JsonClass(generateAdapter = true)
class ProductBarcode(
#PrimaryKey
#ColumnInfo(name = "id")
#Json(name = "Id")
val id: String = "",
#ColumnInfo(name = "product_id", index = true)
#Json(name = "ProductId")
var ProductId: String = "",
#ColumnInfo(name = "barcode", index = true)
#Json(name = "Barcode")
var barcode: String = ""
) {
companion object {
const val TABLE_NAME = "product_barcode"
}
}
ExtensionFun:
suspend fun <T> Call<T>.await(): Response<T> = suspendCoroutine { continuation ->
val callback = object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) =
continuation.resumeNormallyOrWithException {
if (response.isSuccessful || response.code() == 404) {
return#resumeNormallyOrWithException response
} else {
throw IllegalStateException("Http error ${response.code()}, request:${request().url()}")
}
}
}
enqueue(callback)
}
ApiModule:
#Provides
#Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(DateConverter())
.add(BigDecimalConverer())
.add(KotlinJsonAdapterFactory())
.build()
}
fun provideIntegApi(
#Named("base_url") url: String, moshi: Moshi,
prefManager: PrefManager
): IntegApi {
var builder = OkHttpClient.Builder()
builder = BuildTypeInitializations.setupInterceptor(builder)
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${prefManager.token}")
.addHeader("Connection", "close")
.build()
val response = chain.proceed(request)
return#addInterceptor response
}
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
.connectTimeout(5, TimeUnit.MINUTES)
.retryOnConnectionFailure(true)
return Retrofit.Builder()
.baseUrl(url)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(builder.build())
.build()
.create(IntegApi::class.java)
}
I don't know exactly why, but If the Api send 404 response, after than I get the Unable to crate converter exception, but I the server send the response, the error message is gone.
Update:
If is use this :
#get:GET("Product/GetAllBarcode")
val getAllProductBarcodeAsync: Call<List<ProductBarcode>>
instead of this:
#GET("Product/GetAllBarcode")
suspend fun getAllProductBarcodeAsync(): Call<List<ProductBarcode>>
There won't be error, and everything works fine, but I don't understand what's the problem
Update2
I changed Moshi to Jackson, and it doesn't throw converter error like moshi, but throw Http 404 Error which is more friendlier for me, but I' m not completely satisfied. I created await() fun because of Http 404 errors, and I think this bunch of code skipped because of http 404?
Finally I found the issue, I made a mistake, because I tried to use suspend fun and retrofit Call at the same time. I deleted the suspend keyword, and it works perfectly.
my application crashes when I have no internet connection : I am looking for a method that handles any exception form the retrofit instance like server is not found exception Timeout No internet connection
RequestRepository : my repository which contain all my functions
class RequestRepository {
/** suspend function to get the result of token request*/
suspend fun getToken(userLoginModel: UserLoginModel): Response<TokenResponse> {
return ApiService.APILogin.getToken(userLoginModel)
}
ApiService : contain my Retofit instance
object ApiService {
private var token: String = ""
fun setToken(tk: String) {
token = tk
}
private val okHttpClient = OkHttpClient.Builder().connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS).addInterceptor { chain ->
val chainRequest = chain.request()
val requestBuilder = chainRequest.newBuilder()
.addHeader("authorization", "Token $token")
.method(chainRequest.method, chainRequest.body)
val request = requestBuilder.build()
chain.proceed(request)
}.build()
var gson = GsonBuilder()
.setLenient()
.create()
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(LOGIN_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
val API: WebServicesApi by lazy {
retrofit.create(WebServicesApi::class.java)
}
WebServicesApi : my interface which contain my requests
interface WebServicesApi {
/** get the token from the API*/
#POST("user/login/")
suspend fun getToken(#Body userLoginModel: UserLoginModel): Response<TokenResponse>
}
LoginViewModel : my viewModel class
class LoginViewModel(private val repository: RequestRepository) : ViewModel() {
var tokenResponse: MutableLiveData<Response<TokenResponse>> = MutableLiveData()
/** using coroutine in getToken function to get the token */
fun getToken(userLoginModel: UserLoginModel) {
viewModelScope.launch(Dispatchers.IO) {
val tResponse = repository.getToken(userLoginModel)
tokenResponse.postValue(tResponse)
Log.d(TAG, "getToken: ${userLoginModel.password}")
}
}
}
You can add a Interceptor for handle error like this:
class GlobalErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
if (!response.isSuccessful) {
val statusCode = response.code
when (statusCode) {
//Your handle status code in here
}
}
return response
} catch (ex: IOException) {
// You can replace my code with your exception handler code
return Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1)
.message("Can't connect!").code(500).body(
ResponseBody.create(
"application/json; charset=utf-8".toMediaTypeOrNull(),
""
)
).build()
}
}
}
And you must add this class to OkHttpBuider:
val httpBuilder = OkHttpClient.Builder()
......
httpBuilder.addInterceptor(GlobalErrorInterceptor())
I'm new to Android/Retrofit and Moshi. I'm trying to make a POST call to my API and I have a problem with serialising Date. If you see anything else that needs correcting, please point it out as I'm still learning. Thanks
interface ApiInterface {
#Headers("token: ${Config.API_TOKEN}", "Content-Type: application/json")
#POST("/api/signup")
fun signUpNoEmail(#Body userInfo: UserInfo) : Call<SignUpJson>
}
object ApiService {
private val SERVICE : ApiInterface
init {
val retrofit = Retrofit.Builder()
.baseUrl(Config.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
SERVICE = retrofit.create(ApiInterface::class.java)
}
fun signUpNoEmail(userInfo: UserInfo) : Call<SignUpJson> = SERVICE.signUpNoEmail(userInfo)
SignUpJson.kt
#JsonClass(generateAdapter = true)
data class SignUpJson(val vpn_code: String, val expiry_date: Date)
#JsonClass(generateAdapter = true)
data class UserInfo(val username: String)
MainActivity:
val userInfo = UserInfo(username = deviceId)
ApiService.signUpNoEmail(userInfo).enqueue(object : Callback<SignUpJson> {
override fun onResponse(
call: Call<SignUpJson>,
response: Response<SignUpJson>
) {
response.let { serverResponse ->
if (serverResponse.isSuccessful) {
}
}
}
override fun onFailure(call: Call<SignUpJson>, t: Throwable) {
}
})
Caused by: java.lang.IllegalArgumentException: Unable to create
converter for class SignUpJson
Caused by: java.lang.IllegalArgumentException: Platform class
java.util.Date requires explicit JsonAdapter to be registered