Keep session between 2 retrofit instances - android

In my application, I would like to abstract requests to my API. However, to access my API, the user needs to log in and the server sends back a cookie in the form of a JSESSIONID.
So I have a first Retrofit instance created for this using an interface containing the methods for the user to log in :
interface MyService {
#POST("api/login" )
fun login(#Header("cred")id: String): Completable
companion object {
operator fun invoke(): MyService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val cookieHandler = CookieManager()
val client = OkHttpClient.Builder().addNetworkInterceptor(interceptor)
.cookieJar(JavaNetCookieJar(cookieHandler))
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl("http://mytestapi.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build()
.create(MyService::class.java)
}
}
I would like to create a second Retrofit interface that allows me to make very specific queries, but to do so, the user must be logged in.
How can I pass the MyService login token to my second Retrofit?

Related

Simplifying multiple retrofit instances in Hilt Module

I'm developing an Android application in which my user needs to target 2 different endpoints based on some configuration on their profile. I've refactored the entire application trying to use Clean Architecture concept, and so I used Hilt for DI. At the time I didn't understand how to make the baseUrl dynamic in my retrofit instance inside the AppModule since I will know which endpoint has to be used only after the login (We have a unified login service with firebase and different backends for qa and prod), so I created 2 different instances in this way.
TLDR
I have 2 retrofit instance since I need to use different url, this info can be retrieved only after the unified Login.
How can I improve my AppModule so that I can have only one retrofit instance?
AppModule.kt
private const val BASE_PROD_URL = "some prod url"
private const val BASE_DEV_URL = "some other url"
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Production
#Singleton
#Provides
fun provideProductionMachineDetailApi(): MachineDetailApi {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(BASE_PROD_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MachineDetailApi::class.java)
}
#Development
#Singleton
#Provides
fun provideDevMachineDetailApi(): MachineDetailApi {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(BASE_DEV_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MachineDetailApi::class.java)
}
#Production
#Singleton
#Provides
fun provideProductionMachineDetailMainRepository(
#Production api: MachineDetailApi
): MachineDetailBaseRepository = MachineDetailRepositoryImplementation(api)
#Development
#Singleton
#Provides
fun provideDevMachineDetailMainRepository(
#Development api: MachineDetailApi
): MachineDetailBaseRepository = MachineDetailRepositoryImplementation(api)
#Singleton
#Provides
fun provideDispatchers(): DispatcherProvider = object : DispatcherProvider {
override val main: CoroutineDispatcher
get() = Dispatchers.Main
override val io: CoroutineDispatcher
get() = Dispatchers.IO
override val default: CoroutineDispatcher
get() = Dispatchers.Default
override val unconfined: CoroutineDispatcher
get() = Dispatchers.Unconfined
}
}
#Qualifier
#Retention(AnnotationRetention.BINARY)
annotation class Production
#Qualifier
#Retention(AnnotationRetention.BINARY)
annotation class Development
Then inside my viewmodel I inject both instances and based on a simple condition I call the API, for example:
SomeViewModel.kt
fun downloadMachineList() = viewModelScope.launch(dispatchers.io) {
val apiKey: String = getKey()
val repo = if (isProduction) productionRepository else devRepository
when (val response = repo.getMachineList(apiKey, requestBody)) {
is Resource.Error -> _plantListStateFlow.value =
PlantListEvent.Failure(response.message!!)
is Resource.Success -> {
_plantListStateFlow.value = PlantListEvent.Success
_plantList.postValue(response.data!!)
}
}
}
Is it totally a bad practice if done this way?
How could I handle the dynamic url so that I can have only one retrofit instance?

android connecting restful api with mvvm pattern

I am implementing user login and signup with my custom backend in which i get a token when i signup or login.Then i save that token inside SharedPreferences.
So when i want to call an api, i call it inside Repository which is an kotlin object so that it becomes singleton.
In this app when i call api, i should add a authorization header,get token from SharedPreferences and assign to this header.
object MainRepository {
private var sharedPreferencesRepository: SharedPreferencesRepository
private var retrofit: Retrofit
private lateinit var mainApi:MainApi
init {
sharedPreferencesRepository = SharedPreferencesRepository(MyApplication.context)
val user = sharedPreferencesRepository.getUser()
val httpClient = OkHttpClient.Builder()
.addInterceptor(MyOkHttpClientInterceptor(user.token))
.build()
retrofit =
Retrofit.Builder().baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient)
.build()
mainApi = retrofit.create(MainApi::class.java)
}
fun getSomethingFromApi() = myApi.getSomething()
}
The problem is, when i launch app and login it works just fine. But when i logout, and login again, because MainRepository class is only initialized once application launched, it doesn't get new token from SharedPreferences. I mean retrofit instance is only build once.
So how can i solve this problem? Should i add header dynamically for each api call? (There are so many calls that require Authorization header, that is why i did not add header at each request,instead i added an interceptor)
You can add an interceptor and add your authorization header in the OkhttpClient.Builder().
class RetrofitPrivateService {
var token = SharedPreferencesHelper().getToken()
companion object {
private val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor(ApiLogger())
.setLevel(HttpLoggingInterceptor.Level.BODY)
private val client = OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addInterceptor(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + RetrofitPrivateService().token)
.build()
return chain.proceed(request)
}
})
.addInterceptor(interceptor)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
// function to access your API here
}

Android make POST request with retrofit

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 to Log, the URL I am accessing retrofit

I am using retrofit to communicate with the BE and I want to the log the URL i am hitting and the body I am sending and the response I am getting (like 401, 404).
This is to know if I am hitting the right url what is the header, body and the actual URL to verify I am sending all the right info required
I implemented HttpLoggingInterceptor but that does not log it, or I am logging it the right way
can you suggest please.
object RetrofitBuilder {
private const val BASE_URL = "https://xxxx.xxxxxx.com/"
private fun getRetrofit(): Retrofit {
var interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
AppService
#POST
suspend fun postData(#Url url: String, #Body Data: Data, #Header("Autzn") authHeader: String)
ApiHelper
//NOTE: here i am using the different URL as but not the baseUrl defined on the top
apiService.postData("https://abcd.com/msg/oauth2/123456", Data,
"token")
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.1'
Please suggest how to log the url info
thanks
R
just give HttpLoggingInterceptor.Logger interface.
logging interceptor should be like this.
val loggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.tag("retrofit").d(message)
}
})
This is How I'm logging url in logs
val retrofitApi: RetrofitInterface
get() {
if (retrofitInterface == null) {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(logger).build()
val retrofit = Retrofit.Builder()
.baseUrl(REST_HOST)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder()
.setLenient()
.create()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build()
retrofitInterface = retrofit.create(RetrofitInterface::class.java)
}
return this.retrofitInterface!!
}
I suggest you to use HttpLoggingInterceptor with log level, logging can be heavy task especially with images(printing raw bytes), and with big images(40mb+) can throw OOM exception.
So use logging for debug purpose.
public HttpLoggingInterceptor loggingInterceptor() {
HttpLoggingInterceptor loggingInterceptor
= new HttpLoggingInterceptor(message -> Log.d(TAG, message));
loggingInterceptor.setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE);
return loggingInterceptor;
}

Re-initialization an object provided by Dagger 2

I have an interface called LoginService which is used with Retrofit. There was no accessToken before the user login. When the user has logged in, the LoginService Instance should be updated with acessToken so that the user could log out from the app. The problem is that LoginService class is not updated even though it is not declared as #Singleton. If the user has closed the app and reopen it again, the LoginService got updated and therefore he could log out of the app. How can I reinitialize the LoginService instance as soon as the accessToken has been updated?
The key here is not the reinitialize the instance but to create a separate service whenever the user logs in.
you need to have two different interfaces one for API's pre-login and other for post login.
So one would be Authapi.kt and the other would be Api.kt
So first we need to create OkHttpBuilder for each service,
#Provides
#Singleton
#Named(NetModule.NO_AUTH_CLIENT)
fun provideNoAuthOkHttpClient(
okHttpClientBuilder: OkHttpClient.Builder
): OkHttpClient {
return okHttpClientBuilder.build()
}
#Provides
#Singleton
#Named(NetModule.AUTH_CLIENT)
fun provideAuthOkHttpClient(
okHttpClientBuilder: OkHttpClient.Builder,
tokenInterceptor: NetworkInterceptor
): OkHttpClient {
return okHttpClientBuilder.addInterceptor(tokenInterceptor).build()
}
Network Interceptor is the class where you update your access token for API.
NetworkInterceptor.kt
class NetworkInterceptor #Inject constructor(
val context: Context,
serverBaseUrl: String,
val moshi: Moshi,
val preferences: PreferenceUtility
) : Interceptor {
private fun Request.Builder.setDefaultHeaders(): Request.Builder {
addHeader("App_version_code", BuildConfig.VERSION_CODE.toString())
addHeader("App_version_name", BuildConfig.VERSION_NAME)
addHeader("Mobile_model", Build.MODEL.toString())
addHeader("OS_version", Build.VERSION.SDK_INT.toString())
addHeader("OS_version_release", Build.VERSION.RELEASE.toString())
if (preferences.customerId != -1L)
addHeader("Client_id", preferences.customerId.toString())
return this
}
private fun makeRequestWithAuthTokenAndTimeStamp(request: Request) = request.newBuilder()
.setDefaultHeaders()
.apply {
val oldHeader = request.header("Authorization")
if (oldHeader.isNullOrBlank()) {
val token = if (preferences.authToken.isBlank() || !preferences.customerAuthenticated) BuildConfig.ANONYMOUS_TOKEN else preferences.authToken
addHeader("Authorization", "Bearer $token")
}
}
.url(request.url())
.method(request.method(), request.body())
.build()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// ADD THE TOKEN'S OVER HERE
var response =
chain.proceed(makeRequestWithAuthTokenAndTimeStamp(request))
//You can also add refersh logic here.
return response
}
}
And you NetworkModule.kt will be
#Provides
#Singleton
#Named(NetModule.NO_AUTH_CLIENT)
fun provideNoAuthInterceptorRetrofit(
moshi: Moshi,
#Named(NetModule.NO_AUTH_CLIENT) okHttpClient: OkHttpClient,
debugPreferenceUtility: DebugPreferenceUtility
): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(debugPreferenceUtility.serverBaseUrl)
.client(okHttpClient)
.build()
}
#Provides
#Singleton
#Named(NetModule.AUTH_CLIENT)
fun provideAuthInterceptorRetrofit(
moshi: Moshi,
#Named(NetModule.AUTH_CLIENT) okHttpClient: OkHttpClient,
debugPreferenceUtility: DebugPreferenceUtility
): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(debugPreferenceUtility.serverBaseUrl)
.client(okHttpClient)
.build()
}
#Provides
#Singleton
fun provideApi(#Named(AUTH_CLIENT) retrofit: Retrofit) = retrofit.create(Api::class.java)
#Provides
#Singleton
fun provideAuthApi(#Named(NO_AUTH_CLIENT) retrofit: Retrofit) = retrofit.create(AuthApi::class.java)
So the API's that will be used pre-login will be placed inside AuthApi.kt
like send OTP etc and all the rest API will be placed inside Api.kt and Network Interceptor will take care of adding token.

Categories

Resources