I keep getting error IllegalArgumentException when making minimal changes to transition from AsyncTask (before) to Kotlin Coroutines (after). Note that code is working as expected with AsyncTask.
Note: Retrofit is calling my own .php script that returns some object SimpleResultObject encoded in json String.
Before the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int) : Call<SimpleResultObject>
Activity (inside of AsyncTask):
#Override
protected doInBackground(...) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
val call = service.activitySignUp(activity_id, userId)
call.enqueue(Callback<SimpleResultObject>() {}
Receive object in #onResponse method and normally proceed futher.
After the change:
Retrofit:
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int): SimpleResultObject
Activity:
fun signUp() {
myActivityScope.launch(Dispatchers.Main) {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl(LOCALHOST_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(RetrofitAPI::class.java)
try {
val result = service.activitySignUp(specificResultObject.activityId, userId)
} catch (t:Throwable)
Throws java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #3) for method RetrofitAPI.activitySignUpon service.activitySignUp line call
Note: myActivityScope is costum CoroutineScope that finished when hosting Activity finishes.
I have tried everything I could remember: adding OkHttpClient, changing to MoshiConverterFactory, trying other CoroutineScopes and Dispatchers, ...
EDIT: the problem might be on my .php side due to Exeption being above my argument number (maybe null result?), but don't know why something that worked before wouldn't work now.
Based on responses to the question I made a few modifications to the code and managed to fix the issue. The most important, as #Mohammad Sianaki pointed out, was rising Retrofit version from 25.0.0 to 26.0.0 that solved the problem.
So for everyone else that might get the IllegalArgumentException for the argument above the parameter number - consider checking Retrofit versions.
Special thanks to everyone that helped, especially to #CommonsWare!
The provided code in question has some structural issues.
First of all, it seems that a retrofit object is being created for each API call. So, it should be one for all API calls of the application.
Second, network operations should be executed in non-main threads. In the case of coroutines, they should be called in non-main contexts, like Dispatchers.IO.
Third, I think you should return a Response<SimpleResultObject> instead of SimpleResultObject in API functions.
Supposing above, I wrote some codes hoping to solve the problem. Because I think there are some hidden factors in question information.
build.gradle
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.14.1'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.14.1'
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
}
RetrofitAPI.kt
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface RetrofitAPI {
#FormUrlEncoded
#POST("activity_signup.php")
suspend fun activitySignUp(
#Field("activity_id") activityId: Int,
#Field("user_id") userId: Int
): Response<SimpleResultObject>
// Other api declarations ...
}
BaseApiManager.kt
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.CookieManager
import java.util.concurrent.TimeUnit
abstract class BaseApiManager(endPoint: String) {
protected val retrofitAPI: RetrofitAPI =
createAdapter(endPoint)
.create(RetrofitAPI::class.java)
private fun createAdapter(choice: String): Retrofit {
return Retrofit.Builder()
.baseUrl(choice)
.client(createHttpClient())
.addConverterFactory(GsonConverterFactory.create()).build()
}
companion object {
private fun createHttpClient(): OkHttpClient {
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
val cookieHandler = CookieManager()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient.interceptors().add(interceptor)
httpClient.cookieJar(JavaNetCookieJar(cookieHandler))
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
return httpClient.build()
}
}
}
ApiManager.kt
private const val API_END_POINT = "https://your.webservice.endpoint/"
object ApiManager : BaseApiManager(API_END_POINT) {
suspend fun activitySignUp(
activityId: Int,
userId: Int
) = retrofitAPI.activitySignUp(activityId, userId)
// Other api implementations ...
}
Usage:
fun signUp() {
myActivityScope.launch(Dispatchers.IO) {
ApiManager.activitySignUp(activityId, userId).also { response ->
when {
response.isSuccessful -> {
val result = response.body()
result?.apply {
// do something with the result
}
}
else -> {
val code = response.code()
val message = response.message()
// do something with the error parameters...
}
}
}
}
}
Related
I am learning to make api calls using retrofit, But whenever i am trying to make a get request from he api My app crashes and gives the following errors..
java.lang.IllegalArgumentException: Unable to create call adapter for class java.lang.Object
for method WeatherApi.getweatherbycity
Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
Tried:
* retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
* retrofit2.ExecutorCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:237)
at retrofit2.Retrofit.callAdapter(Retrofit.java:201)
at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:232)
... 29 more
I tried many solutions from stackoverflow but nothing is working i am also new to mvvm and viewmodels stuffs.
class RetrofitInstance {
companion object{
private val retrofit by lazy{
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
}
val api by lazy{
retrofit.create(WeatherApi::class.java)
}
}
}
interface WeatherApi {
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Response<WeatherResponse>
}
i have created the Weather Response class using Json to kotlin converter plugin in android studio
data class WeatherResponse(
val base: String,
val clouds: Clouds,
val cod: Int,
val coord: Coord,
val dt: Int,
val id: Int,
val main: Main,
val name: String,
val sys: Sys,
val timezone: Int,
val visibility: Int,
val weather: List<Weather>,
val wind: Wind
)
class WeatherRepository{
suspend fun getweatherbycityname(cityname:String)=RetrofitInstance.api.getweatherbycity(cityname)
}
class WeatherViewModel(
val weather_rep: WeatherRepository
):ViewModel() {
val current_weather:MutableLiveData<Response<WeatherResponse>> = MutableLiveData()
fun getweatherbycity(city:String="London")= viewModelScope.launch {
val weather=weather_rep.getweatherbycityname(city)
//current_weather.postValue(weather)
}
}
dependencies-
implementation 'com.squareup.retrofit2:retrofit:2.1.0'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
In my case, I was using suspend in my API interface method, however the problem was that I was using Retrofit2 2.3.0 when I should have been using Retrofit2 2.6 or higher, as according to this, in that version they added official support for Coroutines.
My API:
interface CoolApi {
#POST("/api/v1/path")
#Headers("#: Noauth")
suspend fun coolApiCall(#Body request: coolRequest): Response<CoolResponse>?
}
The Retrofit builder:
val api = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(createOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CoolApi::class.java)
And then I launch the Coroutine like this:
lifecycleScope.launch(Dispatchers.IO) {
try {
val response = apiMethod(api)
if (response!!.isSuccessful) {
callback.onSuccess(response.code(), response.body())
} else {
// Error handling
}
} catch (e: Exception) {
when (e) {
// Exception handling
}
}
}
Beware that in order to use lifecycleScope you need to add some dependencies. In my gradel.build (:app) I have:
// For lifecycle-aware coroutines
def lifecycle_version = "2.3.1"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.31"
And then you can use lifecycleScope from an Activity or Fragment. If you're launching the Coroutine in another file (which was my case), you can pass it as an argument.
You are incorrectly to get the response here:
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Response<WeatherResponse>
Because you're using RxJavaCallAdapterFactory please return observeable for the response
#GET("data/2.5/weather")
suspend fun getweatherbycity(
#Query("q")
cityname: String="London",
#Query("appid")
api_key: String = API_KEY
): Single<Response<WeatherResponse>>
interface WeatherAp make it public
it s happened same things to me, i just updated retrofit version from build.gradle and works. try to update retrofit version.
I'm trying to make my first POST request to make the user login using retrofit library, but it's not working and i don't understand why. If i make a GET request it works, but with POST something gone wrong and i don't understand why. My API run on localhost webserver
My code of the LoginService:
private const val BASE_URL = "http://localhost:10000/api/"
/**
* Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
* full Kotlin compatibility.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* Use the Retrofit builder to build a retrofit object using a Moshi converter with our Moshi
* object.
*/
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface LoginApiService {
#Headers("Content-Type: application/json")
#POST("login")
suspend fun makeLogin(#Body usr: User): LoginResponse
}
/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object LoginApi {
val retrofitService : LoginApiService by lazy { retrofit.create(LoginApiService::class.java) }
}
code of the LoginResponse class
data class LoginResponse(
val token: String,
val expiration: Date,
val role: Int)
code of the User class:
data class User(
val mail: String,
val pw: String
) : Parcelable
Code of the ViewModel that make the request:
private fun makeLogin(email: String, password: String) {
viewModelScope.launch {
try {
val usr = User(email, password)
val rsp = LoginApi.retrofitService.makeLogin(usr)
_isLogged.value = true
} catch (ex: Exception) {
_status.value = LoginStatus.ERROR
}
}
}
Can someone help me to solve this please? it seems that the request it's not sended.
my retrofit call generate this error in logcat in the try-catch block
java.lang.IllegalArgumentException: Unable to create converter for class com.example.ticketapp.network.LoginResponse
for method LoginApiService.makeLogin
Default Retrofit's timeout is 10sec. You can fix it like this:
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
...
.client(client)
.build()
Here I set it to 30secs, but you can use any number and TimeUnit you want.
UPD:
You can store Retorfit builder in a separate file like this:
interface WebService {
companion object {
fun <T> build(clazz: Class<T>): T {
val client = OkHttpClient.Builder()
...
.build()
val retrofit = Retrofit.Builder()
...
.build()
return retrofit.create(clazz)
}
}
}
Then you can have multiple ApiService interfaces. And use them like this:
val myApiService = WebService.build(MyApiServiceInterface::class.java)
myApiService.myRequestFunction()
Try to add
android:usesCleartextTraffic="true"
Into your application tag in manifest
How can create androidTest for sample retrofit request?
Sample
data class TestDataClass(
val id: String,
val employee_name: String,
val employee_salary: String,
val employee_age: String,
val profile_image: String)
enum class NetworkState { LOADING, ERROR, DONE }
private const val BASE_URL = "http://dummy.restapiexample.com/api/v1/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface TestApiService {
#GET("employees")
fun getPropertiesAsync():
Deferred<List<TestDataClass>>
}
object TestApi {
val retrofitTest : TestApiService by lazy { retrofit.create(TestApiService::class.java) }
}
You can use the MockWebServer library by Square.
Create a resources in your tests source set (src/test/resources), and put in it a JSON file containing a sample response from your API. Let's say it looks like this:
src/test/resources/sample_response.json
[
{
"id": "1",
"employee_name": "John Doe",
"employee_salary": "60000",
"employee_age": 37,
"profile_image": "https://dummy.sample-image.com/johndoe"
}
]
You may then write your tests as:
class ApiTest {
private lateinit var server: MockWebServer
private lateinit var retrofit: Retrofit
private lateinit var service: TestApiService
#Before
fun setup() {
server = MockWebServer()
retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(get<Moshi>()))
.addCallAdapterFactory(get<CoroutinesNetworkResponseAdapterFactory>())
.baseUrl(mockWebServer.url("/"))
.build()
service = retrofit.create(TestApi::class.java)
}
#After
fun teardown() {
server.close()
}
#Test
fun simpleTest() = runBlocking<Unit> {
val sampleResponse = this::class.java.getResource("/sample_response.json").readText()
server.enqueue(
MockResponse()
.setBody(sampleResponse)
)
val response = service.getPropertiesAsync().await()
assertTrue(1, response.size)
assertTrue(response[0].employee_name = "John Doe"
// Make as many assertions as you like
}
}
You have to ask yourself though, what exactly is it that you're trying to test? There's no need to test Retrofit's functionality. Nor should you test functionality of other well known libraries like Moshi.
These tests best serve the purpose of validating that the data models you have created for API responses are indeed correct, and that your parser (in this case, Moshi) can correctly handle unexpected values (such as null) gracefully. It is therefore important that the sample responses that you pick are actual responses from your API, so that your data models can be validated against real data in tests before being used in the app.
I have an Android application written in Kotlin, that gets data from an API, for now it's just a local hosted JSON file. When I'm trying to get the data, I receive the error that my list, persons, is not initialized thus persons == null and didn't receive the data. I'm not sure what I did wrong and how to fix this.
The model
data class Person (
#Json(name = "personId")
val personId: Int,
#Json(name = "personName")
val name: String,
#Json(name = "personAge")
val age: Int,
#Json(name = "isFemale")
val isFemale: Boolean,
)
The JSON response
{
"persons": [{
"personId": 1,
"personName": "Bert",
"personAge": 19,
"isFemale": "false",
}
]
}
The ApiClient
class ApiClient {
companion object {
private const val BASE_URL = "http://10.0.2.2:3000/"
fun getClient(): Retrofit {
val moshi = Moshi.Builder()
.add(customDateAdapter)
.build()
return Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
}
}
The Retrofit http methods
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<List<Person>>
}
and finally the call
class PersonActivity: AppCompatActivity(){
private lateinit var jsonAPI: ApiInterface
private lateinit var persons: List<Person>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_person)
val retrofit = ApiClient.getClient()
jsonAPI = retrofit.create(ApiInterface::class.java)
jsonAPI.getPersons()
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ persons = it })
}
}
Expected: Data from the JSON file into the persons list instead of NULL.
Your moshi adapter is expecting the person objects directly but they are nested inside the "persons" Json array.
You should add another model as follow
data class Persons (
#Json(name="persons")
val persons : List<Person>
)
Also change the interface to return the new object.
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<Persons>
}
Will also need to change the subscription to
.subscribe({ persons = it.persons })
I think there could be one of two issues.
If you are using moshi reflection you will have something like these dependencies in gradle:
//Moshi Core
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Reflection
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
In that case you will need to use the KotlinJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(customDateAdapter)
.add(KotlinJsonAdapterFactory())
.build()
If you are using codegen you'll need this:
apply plugin: 'kotlin-kapt'
...
//Moshi Core Artifact
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Codegen
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
Additionally, every class you want to deserialise to should be annotated with
#JsonClass(generateAdapter = true) (so the Person and Persons class)
Hi I am trying to learn rxjava2. I am trying to call API's using rxjava2 and using retrofit for building URL and converting JSON into Moshi.
I want to use Observable pattern with retrofit. Does anyone know whats way to do it ? Any standard and best approach like wrapper for error handling and all ?
AppModule.kt
#Provides
#Singleton
fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.build()
}
ApiHelperImpl.kt
#Inject
lateinit var retrofit: Retrofit
override fun doServerLoginApiCall(email: String, password: String): Observable<LoginResponse> {
retrofit.create(RestApi::class.java).login(email, password)
}
I am calling doServerLoginApiCall from the LoginViewModel like below
LoginViewModel.kt
fun login(view: View) {
if (isEmailAndPasswordValid(email, password)) {
ApiHelperImpl().doServerLoginApiCall(email, password)
}
}
RestApi.kt
interface RestApi {
#FormUrlEncoded
#POST("/partner_login")
fun login(#Field("email") email: String, #Field("password") password: String): Call<LoginResponse>
}
LoginResponse.kt
data class LoginResponse(
#Json(name = "code")
val code: Int? = null,
#Json(name = "otp_verify")
val otpVerify: Int? = null,
#Json(name = "data")
val userDetails: UserDetails? = null,
#Json(name = "message")
val message: String? = null,
#Json(name = "status")
val status: String? = null
)
This is the rough idea to show you how to use Retrofit2 with RxJava2. you can find a lot of tutorial in google.
Step 1:
Add the following dependencies to your gradle file
// Rx stuff
compile "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
compile "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
// retrofit
compile "com.squareup.retrofit2:retrofit:$retrofitVersion"
compile "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
compile "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
Step 2: Create you Retrofit API interface like you do but it has a bit difference that is the return type should be Observable<LoginResponse> not a Call<LoginResponse>
interface RestApi {
#FormUrlEncoded
#POST("/partner_login")
fun login(#Field("email") email: String, #Field("password") password: String): Observable<LoginResponse>
}
Step 3:
build you retrofit API object:
retrofit.create(RestApi::class.java).login(email, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe{ loginResponse ->
// TODO deal with your response here
}
Do not you just know how to return results?
The way to return results with rx is as follows.
ApiHelperImpl().doServerLoginApiCall(email, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
// doSomething
}
subscribeOn calls api in a other thread.
observeOn is a process for processing subscribe in the main thread.
subscribe has multiple overloading methods. Please check the document.
You can refer to the following article: [link]:https://medium.com/3xplore/handling-api-calls-using-retrofit-2-and-rxjava-2-1871c891b6ae