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.
Related
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!
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'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
I am developing a news app and I want to add two modules in application class but I am getting the following exception.
java.lang.RuntimeException: Unable to create application yodgorbek.komilov.musobaqayangiliklari.di.application.SportNewsApplication: org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one: [type:Single,primary_type:'yodgorbek.komilov.musobaqayangiliklari.internet.SportNewsInterface']
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5971)
at android.app.ActivityThread.access$1300(ActivityThread.java:206)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1700)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6820)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:922)
Caused by: org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one: [type:Single,primary_type:'yodgorbek.komilov.musobaqayangiliklari.internet.SportNewsInterface']
at org.koin.core.registry.BeanRegistry.addDefinition(BeanRegistry.kt:144)
at org.koin.core.registry.BeanRegistry.saveDefinition(BeanRegistry.kt:101)
at org.koin.core.registry.BeanRegistry.saveDefinitions(BeanRegistry.kt:71)
at org.koin.core.registry.BeanRegistry.loadModules(BeanRegistry.kt:49)
at org.koin.core.KoinApplication.loadModulesAndScopes(KoinApplication.kt:66)
at org.koin.core.KoinApplication.modules(KoinApplication.kt:60)
at yodgorbek.komilov.musobaqayangiliklari.di.application.SportNewsApplication$onCreate$1.invoke(SportNewsApplication.kt:19)
at yodgorbek.komilov.musobaqayangiliklari.di.application.SportNewsApplication$onCreate$1.invoke(SportNewsApplication.kt:11)
at org.koin.core.context.GlobalContextKt.startKoin(GlobalContext.kt:72)
at yodgorbek.komilov.musobaqayangiliklari.di.application.SportNewsApplication.onCreate(SportNewsApplication.kt:16)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1155)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5966)
... 8 more
below SportNewsApplication.kt class
class SportNewsApplication : Application() {
override fun onCreate() {
super.onCreate()
// Adding Koin modules to our application
startKoin {
// androidContext(this#SportNewsApplication)
modules(
listOf(appModules, bbcModules))
}
}
}
below appModules.kt
const val BASE_URL = "https://newsapi.org/"
val appModules = module {
// The Retrofit service using our custom HTTP client instance as a singleton
single {
createWebService<SportNewsInterface>(
okHttpClient = createHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = BASE_URL
)
}
// Tells Koin how to create an instance of CatRepository
factory<NewsRepository> { (NewsRepositoryImpl(sportsNewsApi = get())) }
// Specific viewModel pattern to tell Koin how to build MainViewModel
viewModel { MainViewModel(newsRepository = get()) }
}
/* Returns a custom OkHttpClient instance with interceptor. Used for building Retrofit service */
fun createHttpClient(): OkHttpClient {
val client = OkHttpClient.Builder()
client.readTimeout(5 * 60, TimeUnit.SECONDS)
return client.addInterceptor {
val original = it.request()
val requestBuilder = original.newBuilder()
requestBuilder.header("Content-Type", "application/json")
val request = requestBuilder.method(original.method, original.body).build()
return#addInterceptor it.proceed(request)
}.build()
}
/* function to build our Retrofit service */
inline fun <reified T> createWebService(
okHttpClient: OkHttpClient,
factory: CallAdapter.Factory, baseUrl: String
): T {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addCallAdapterFactory(factory)
.client(okHttpClient)
.build()
return retrofit.create(T::class.java)
}
below bbcModules.kt
const val base_url = "https://newsapi.org/"
val bbcModules = module {
// The Retrofit service using our custom HTTP client instance as a singleton
single {
createBBCWebService<SportNewsInterface>(
okHttpClient = createBBCHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = base_url
)
}
// Tells Koin how to create an instance of CatRepository
factory<BBCRepository> { (BBCRepositoryImpl(bbcsportNewsApi = get())) }
// Specific viewModel pattern to tell Koin how to build MainViewModel
viewModel { BBCSportViewModel(bbcRepository = get()) }
}
/* Returns a custom OkHttpClient instance with interceptor. Used for building Retrofit service */
fun createBBCHttpClient(): OkHttpClient {
val client = OkHttpClient.Builder()
client.readTimeout(5 * 60, TimeUnit.SECONDS)
return client.addInterceptor {
val original = it.request()
val requestBuilder = original.newBuilder()
requestBuilder.header("Content-Type", "application/json")
val request = requestBuilder.method(original.method, original.body).build()
return#addInterceptor it.proceed(request)
}.build()
}
/* function to build our Retrofit service */
inline fun <reified T> createBBCWebService(
okHttpClient: OkHttpClient,
factory: CallAdapter.Factory, baseUrl: String
): T {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addCallAdapterFactory(factory)
.client(okHttpClient)
.build()
return retrofit.create(T::class.java)
}
what I have tried
1.clean rebuild and invalidate cache restart and other StackOverflow answers
it did not solve my problem.
I have followed following link https://github.com/InsertKoinIO/koin/issues/420 as well
I want to know what I have to do in order to solve the exception.
It looks like you try to create two instances of OkHttpClient in separate modules. You can use override parametr for a module for override one instance by other
(module(override = true))
but in this case it is incorrect. You must have tow different instances OkHttpClient. For this, you can use named instance
single<OkHttpClient>(named("WebService")) {
createWebService<SportNewsInterface>(
okHttpClient = createHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = BASE_URL
)
}
and
single<OkHttpClient>(named("BBCWebService")) {
createBBCWebService<SportNewsInterface>(
okHttpClient = createBBCHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = base_url
)
}
If need inject specific client need to use
SomeClassNeedDependency(get(named("WebService")))
More information
Try to use named. Here is a doc
below appModules.kt
single(named("appModules")) {
createBBCWebService<SportNewsInterface>(
okHttpClient = createBBCHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = base_url
)
}
factory<NewsRepository> { (NewsRepositoryImpl(sportsNewsApi = get(named("appModules")))) }
below bbcModules.kt
single(named("bbcModules")) {
createBBCWebService<SportNewsInterface>(
okHttpClient = createBBCHttpClient(),
factory = RxJava2CallAdapterFactory.create(),
baseUrl = base_url
)
}
factory<BBCRepository> { (BBCRepositoryImpl(bbcsportNewsApi = get(named("bbcModules")))) }
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...
}
}
}
}
}