I am trying to make repetitive calls with a timeout of 5 seconds with Retrofit2 on Android. The interface:
import com.google.gson.GsonBuilder
import io.reactivex.Observable
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface YooApiService {
#GET("payment/getInfo.php")
fun search(#Query("q") query: String,
#Query("apicall") apicall: String): Observable<String>
/**
* Companion object to create the GithubApiService
*/
companion object Factory {
fun create(): YooApiService {
val httpClient = OkHttpClient.Builder()
.callTimeout(5, TimeUnit.SECONDS)
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
val gson = GsonBuilder()
.setLenient()
.create()
val builder = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("https://pros.sbs/")
builder.client(httpClient.build())
val retrofit = builder.build()
return retrofit.create(YooApiService::class.java)
}
}
Search Repository
import io.reactivex.Observable
class SearchRepository(val apiService: YooApiService) {
fun searchUsers(id: String): Observable<String> {
return apiService.search(query = id, apicall = "status")
}
}
Function which I call in my Activity:
private fun payment(id: String) {
val repository = SearchRepositoryProvider.provideSearchRepository()
repository.searchUsers(id)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe ({
status : String ->
if (status == "succeeded") {
// do staff
}
}, { error ->
Log.i(TAG, error.message!!)
})
}
The thing is, only one call is made after function initialization. How can I fix this so that multiple calls were made? Thank you!
Related
learning about retrofit but couldn't write the tests for it. I came from jest background and struggling to test two things:
that the call was making to a specific end point and its status.
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherAPI {
#GET("current.json")
suspend fun getCurrentWeatherData(#Query("key") apiKey: String, #Query("q") cityName: String, #Query("qui") quiValue: String): Response<ResponseBody>
companion object {
private const val BASE_URL = "http://api.weatherapi.com/v1/"
val instance: WeatherAPI by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherAPI::class.java)
}
}
}
Test file:
class WeatherAPITest {
#Test
fun WeatherAPI_getCurrentWeather_apiKey_city(){
runBlocking {
val res = WeatherAPI.instance.getCurrentWeatherData("123", "London", "no")
assertThat(res.code()).isEqualTo(200)
}
}
}
it fails because there is no token. How can i mock the actual api call to return say 200 on the test and confirm something like this:
assertThat(url).isEqualTo('http:....?key=123&q=London')
assertThat(responseCode).isEqualTo(200)
I have a Retrofit Kotlin singleton where I need a context to access the cacheDir, what would be the best current approach to solve this?:
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import java.io.File
import java.util.concurrent.TimeUnit
private const val BASE_URL = "https://5c5c8ba5345018a0014aa1b24.mockapi.io/api/test"
/**
* Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
* full Kotlin compatibility.
*/
private val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
// Create a cache object
const val cacheSize = 1 * 1024 * 1024 // 1 MB
val httpCacheDirectory = File(cacheDir, "http-cache")
val cache = Cache(httpCacheDirectory, cacheSize.toLong())
// create a network cache interceptor, setting the max age to 1 minute
private val networkCacheInterceptor = Interceptor { chain ->
val response = chain.proceed(chain.request())
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build()
response.newBuilder()
.header("Cache-Control", cacheControl.toString())
.build()
}
// Create the logging interceptor
private val loggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
// Create the httpClient, configure it
// with cache, network cache interceptor and logging interceptor
// TODO: don't use loggingInterceptor in release build.
private val httpClient = OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor(networkCacheInterceptor)
.addInterceptor(loggingInterceptor)
.build()
// Create the Retrofit with the httpClient
private val retrofit = Retrofit.Builder()
.baseUrl("http://localhost/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(httpClient)
.build()
/**
* A public interface that exposes the [getWeatherForecasts] method
*/
interface WeatherForecastsApiService {
#GET(BASE_URL)
suspend fun getWeatherForecasts(): List<WeatherForecastEntity>
}
/**
* A public Api object that exposes the lazy-initialized Retrofit service.
*/
object WeatherForecastApi {
val RETROFIT_SERVICE : WeatherForecastsApiService by lazy {
retrofit.create(WeatherForecastsApiService::class.java)
}
}
Simply store a static instance of your application class in it an access it where you want -> not ideal solution but it works* Source
open class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
lateinit var instance: MyApplication
private set
fun getApplicationContext(): Context? {
return instance.applicationContext
}
}
}
Use it like this:
MyApplication.instance.cacheDir()
*you would introduce a direct dependency between your components and
your project's Application class, making it impossible to use these
components with another Application class and impossible to declare
these components inside a separate gradle module for example. - LINK
Using DI for example with Hilt -> to much overhead for small projects?
Custom Kotlin singleton implementation -> readability and complexity drawback.
I'm stuck at Exception that appears in the title. I checked similar topics, but they were specific cases that didn't apply to my situation. Below you could see my models, retrofit setup and its usage. I tried removing Body Class, Response Class, just to check if they are culprits, but unfortunately those weren't the case. Maybe someone will be able to figure out what I'm doing wrong?
Stack Trace:
java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #2)
for method ApiService.login
at retrofit2.Utils.methodError(Utils.java:52)
at retrofit2.Utils.methodError(Utils.java:42)
at retrofit2.Utils.parameterError(Utils.java:61)
at retrofit2.RequestFactory$Builder.parseParameter(RequestFactory.java:311)
at retrofit2.RequestFactory$Builder.build(RequestFactory.java:182)
at retrofit2.RequestFactory.parseAnnotations(RequestFactory.java:65)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:25)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:168)
at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy1.login(Unknown Source)
at com.rudearts.cyber2020.services.NetworkService$login$1.invokeSuspend(NetworkService.kt:17)
at com.rudearts.cyber2020.services.NetworkService$login$1.invoke(Unknown Source:10)
at kotlinx.coroutines.flow.SafeFlow.collect(Builders.kt:56)
at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:144)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:111)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:33)
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)
Models:
import com.google.gson.annotations.SerializedName
data class LoginRequest(
#SerializedName("pin") val pin:String,
#SerializedName("pushId") val pushId:String)
import com.google.gson.annotations.SerializedName
data class UserJson (
#SerializedName("id") val id:Long,
#SerializedName("name") val name:String?,
#SerializedName("access_rights") val accessRights:String?)
Retrofit Builder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitBuilder {
private const val BASE_URL = "<url>"
private val client = OkHttpClient.Builder().build()
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
import com.rudearts.cyber2020.model.LoginRequest
import com.rudearts.cyber2020.model.UserJson
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface ApiService {
#Headers("Content-Type: application/json")
#POST("login.php")
suspend fun login(#Body request: LoginRequest):Response<UserJson>
}
Usage in other class:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.*
class NetworkService {
private val repoService by lazy { RepoService.instance }
private val apiService = RetrofitBuilder.apiService
fun login(pin:String, token:String): Flow<NetworkResult<Boolean>> = flow {
emit(Loading)
try {
val userJson = apiService.login(LoginRequest(pin,token)).body()
userJson?.let {
val user =
User(userJson.id, userJson.name ?: "", emptyList(), userJson.accessRights ?: "")
repoService.user = user
}
emit(NetworkSuccess(userJson != null))
} catch (throwable: Throwable) {
emit(NetworkError(throwable))
}
}
}
It could be that you are using an older version of okhttp / retrofit as you are using suspend function it requires the latest version of both libraries.
Try call the function like this
fun login(LoginRequest(pin,token))
instead of this
fun login(pin:String, token:String)
the exception says the error is on the second parameter
I'm struggling with making 2 different interceptors to my retrofit client
i have already one interceptor to add my query api_key into the request and im trying to add the
HttpLoggingInterceptor
with the same retrofit instance is there a way to do this ?
this is my code
import com.example.tvapptest.Services.MovieService
import com.example.tvapptest.Services.ShowService
import com.example.tvapptest.Utils.Constants
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitConfig {
private val interceptor : HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
}
private val client : OkHttpClient = OkHttpClient.Builder().apply {
this.addInterceptor(interceptor)
}.build()
private val clientapi : OkHttpClient = OkHttpClient.Builder().apply {
this.addNetworkInterceptor(ApiInterceptor())
}.build()
// use lazy to insure that only one instance of retrofit will be used - no duplication
private val retrofit : Retrofit by lazy {
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Constants.BASE_URL)
// the issue is here how can i add another interceptor to the same client here
//.client(client)
.client(clientapi)
.build()
}
val movieService : MovieService by lazy {
retrofit.create(MovieService::class.java)
}
val showService : ShowService by lazy {
retrofit.create(ShowService::class.java)
}
}
and this is my ApiInterceptor class
package com.example.tvapptest.Network
import com.example.tvapptest.Utils.Constants
import okhttp3.Interceptor
import okhttp3.Response
// this class is used to intercept the request and add the query param api_key
class ApiInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val originalHttpUrl = original.url
val requestBuilder = original.newBuilder().url(originalHttpUrl.newBuilder().addQueryParameter("api_key",Constants.API_KEY).build())
return chain.proceed(requestBuilder.build())
}
}
Is there any reasons for wanting two different clients?
Seems like you would be fine with using just one and adding both the interceptors to the same client.
This is something in the lines of what it looks like in kotlin.
OkHttpClient.Builder()
.addInterceptor(interceptor)
.addNetworkInterceptor(ApiInterceptor())
.build()
}
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...
}
}
}
}
}