Accessing sharedPreferences inside Retrofit Interceptor - android

Here is a Retrofit Interceptor used to inject automatically a token inside requests. I'm trying to get this token from sharedPreferences but getSharedPreferences is not available there.
How can i retrieve my token from sharedpreferences inside this Interceptor ?
import android.preference.PreferenceManager
import okhttp3.Interceptor
import okhttp3.Response
class ServiceInterceptor: Interceptor {
var token : String = "";
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if(request.header("No-Authentication") == null){
if (request.url.toString().contains("/user/signin") === false) {
// Add Authorization header only if it's not the user signin request.
// Get token from shared preferences
val sharedPreference = PreferenceManager.getSharedPreferences()
token = sharedPreference.getString("token")
if (!token.isNullOrEmpty()) {
val finalToken = "Bearer " + token
request = request.newBuilder()
.addHeader("Authorization", finalToken)
.build()
}
}
}
return chain.proceed(request)
}
}

There's a simple solution for this in Kotlin – just copy & paste the code into a new AppPreferences.kt file and follow the 4 TODO steps outlined in the code:
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
object AppPreferences {
private var sharedPreferences: SharedPreferences? = null
// TODO step 1: call `AppPreferences.setup(applicationContext)` in your MainActivity's `onCreate` method
fun setup(context: Context) {
// TODO step 2: set your app name here
sharedPreferences = context.getSharedPreferences("<YOUR_APP_NAME>.sharedprefs", MODE_PRIVATE)
}
// TODO step 4: replace these example attributes with your stored values
var heightInCentimeters: Int?
get() = Key.HEIGHT.getInt()
set(value) = Key.HEIGHT.setInt(value)
var birthdayInMilliseconds: Long?
get() = Key.BIRTHDAY.getLong()
set(value) = Key.BIRTHDAY.setLong(value)
private enum class Key {
HEIGHT, BIRTHDAY; // TODO step 3: replace these cases with your stored values keys
fun getBoolean(): Boolean? = if (sharedPreferences!!.contains(name)) sharedPreferences!!.getBoolean(name, false) else null
fun getFloat(): Float? = if (sharedPreferences!!.contains(name)) sharedPreferences!!.getFloat(name, 0f) else null
fun getInt(): Int? = if (sharedPreferences!!.contains(name)) sharedPreferences!!.getInt(name, 0) else null
fun getLong(): Long? = if (sharedPreferences!!.contains(name)) sharedPreferences!!.getLong(name, 0) else null
fun getString(): String? = if (sharedPreferences!!.contains(name)) sharedPreferences!!.getString(name, "") else null
fun setBoolean(value: Boolean?) = value?.let { sharedPreferences!!.edit { putBoolean(name, value) } } ?: remove()
fun setFloat(value: Float?) = value?.let { sharedPreferences!!.edit { putFloat(name, value) } } ?: remove()
fun setInt(value: Int?) = value?.let { sharedPreferences!!.edit { putInt(name, value) } } ?: remove()
fun setLong(value: Long?) = value?.let { sharedPreferences!!.edit { putLong(name, value) } } ?: remove()
fun setString(value: String?) = value?.let { sharedPreferences!!.edit { putString(name, value) } } ?: remove()
fun exists(): Boolean = sharedPreferences!!.contains(name)
fun remove() = sharedPreferences!!.edit { remove(name) }
}
}
Now from anywhere within your app you can get a value like this:
val heightInCentimeters: Int? = AppPreferences.heightInCentimeters
val heightOrDefault: Int = AppPreferences.heightInCentimeters ?: 170
Setting a value to the SharedPreferences is just as easy:
AppPreferences.heightInCentimeters = 160 // sets a new value
The above is extracted from my FitnessTracker project. See this file for a full example.

As coroutineDispatcher has commented you should pass in the shared preferences into the interceptor's constructor and hold a reference to them.
Try this:
class ServiceInterceptor(private val prefs: SharedPreferences): Interceptor {
val token: String get() = prefs.getString("token")
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if(request.header("No-Authentication") == null){
if (request.url.toString().contains("/user/signin") === false) {
// Add Authorization header only if it's not the user signin request.
request = token
.takeUnless { it.isNullOrEmpty }
?.let {
request.newBuilder()
.addHeader("Authorization", "Bearer $it")
.build()
}
?: request
}
}
return chain.proceed(request)
}
}
The interceptor now takes in a reference to shared preferences so the dependency has been inverted and it can allow for easy testing by stubbing the passed SharedPreferences.
And it can be instanatiated like this:
ServiceInterceptor(PreferenceManager.getSharedPreferences())

You can create a singleton class for SharedPreference and then you can access it from any class you want.
Example
class SessionManager private constructor(context:Context) {
private val prefs:SharedPreferences
private val editor:SharedPreferences.Editor
var token:String
get() {
return prefs.getString("token", "")
}
set(token) {
editor.putString("token", token)
editor.apply()
}
init{
prefs = context.getSharedPreferences("Your_Preference_name", Context.MODE_PRIVATE)
editor = prefs.edit()
}
companion object {
private val jInstance:SessionManager
#Synchronized fun getInstance(context:Context):SessionManager {
if (jInstance != null)
{
return jInstance
}
else
{
jInstance = SessionManager(context)
return jInstance
}
}
}
}
Now you have to pass context in constructor of ServiceInterceptor and you can access SharedPreference like following.
val token = SessionManager.getInstance(context).token;

try this
val token = PreferenceManager.getSharedPreferences().getToken("","")
builder.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
.addHeader("Authorization", "Bearer $token")
val request = requestBuilder.build()
chain.proceed(request)
}
return builder.build()

I needed to store JWT token in my application and I was struggling with the same issue and came up with a solution, it may be useful for some of you.
It's achievable with dependency injection using Dagger Hilt.
Inject ServiceInterceptor to RetrofitClient
class RetrofitClient #Inject constructor(
private val serviceInterceptor: ServiceInterceptor,
) {
val api: ApiInterface by lazy {
Retrofit.Builder()
.addConverterFactory(
GsonConverterFactory.create(
GsonBuilder().registerTypeAdapter(
LocalDate::class.java,
JsonDeserializer { json, _, _ ->
LocalDate.parse(
json.asJsonPrimitive.asString
)
}).create(),
)
)
.baseUrl(Constants.URL)
.client(
OkHttpClient.Builder()
.addInterceptor(OkHttpProfilerInterceptor())
.addInterceptor(serviceInterceptor)
.build()
)
.build()
.create(ApiInterface::class.java)
}
}
Inject SharedPreferences to ServiceInterceptor
class ServiceInterceptor #Inject constructor(
private val sharedPreferences: SharedPreferences,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = sharedPreferences.getString("JWT_AUTH_TOKEN", "")
var request = chain.request()
if (request.header("No-Authentication") == null) {
if (token != null && token.isNotEmpty()) {
val bearerToken = "Bearer $token"
request = request.newBuilder()
.addHeader("Authorization", bearerToken)
.build()
}
}
return chain.proceed(request)
}
}
Define Module for dagger hilt:
#Module
#InstallIn(SingletonComponent::class)
object ConfigModule {
#Singleton
#Provides
fun provideSharedPreferences(#ApplicationContext context: Context): SharedPreferences =
context.getSharedPreferences("JWT_AUTH_TOKEN", Context.MODE_PRIVATE)
#Singleton
#Provides
fun provideServiceInterceptor(sharedPreferences: SharedPreferences): ServiceInterceptor =
ServiceInterceptor(sharedPreferences)
#Singleton
#Provides
fun provideRetrofitClient(serviceInterceptor: ServiceInterceptor): RetrofitClient =
RetrofitClient(serviceInterceptor)
}
That's it, you should be good to go with injecting your RetrofitClient into your repository like that:
class CustomRepository #Inject constructor(
private val retrofitClient: RetrofitClient,
)
If you want to write to SharedPreferences from other classes just inject it:
#AndroidEntryPoint
class LoginUserFragment #Inject constructor(
private val sharedPreferences: SharedPreferences,
)
And then to write your JWT token to SharedPreferences use code:
with(sharedPreferences.edit()) {
putString("JWT_AUTH_TOKEN", token)
apply()
}

Related

How to save retrofit response in shared preferences in and use it repository layer

I have post method where I am sending login request to server but first I want to save that response using shared preferences in my repository how can save retrofit response in shared preferences
below my interface class I have implemented login post request logic
interface MeloApi {
#Headers("Content-Type: application/json")
#POST("/login")
suspend fun makeLogin(#Body loginModel: LoginModel) : Response<LoginModel>
}
below my loginModel class
data class LoginModel(
val userName:String,
val password:String)
below LoginResponse.kt
data class LoginResponse(
#SerializedName("accessToken")
val accessToken: String,
#SerializedName("refreshToken")
val refreshToken: String,
#SerializedName("status")
val status: String,
#SerializedName("user")
val user: User
)
below my Interceptor
class HeaderInterceptor(
private val tokenManager: TokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request.newBuilder()
.header("refreshToken", tokenManager.refreshToken.toString())
.header("accessToken", tokenManager.accessToken.toString())
.build()
return chain.proceed(request)
}
}
below my token manager
class TokenManager {
var accessToken: String? = null
var refreshToken: String? = null
}
below my appModule.kt
val apiModule = module {
single {
TokenManager()
}
single {
HeaderInterceptor(get())
}
single {
val httpInterceptor = HttpLoggingInterceptor()
httpInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
Retrofit.Builder()
.client(
OkHttpClient.Builder()
.addInterceptor(HeaderInterceptor(get()))
.addInterceptor(httpInterceptor).build()
)
// .addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
.create(MeloApi::class.java)
}
}
below my LoginRepository
class LoginRepository(
private val meloApi: MeloApi
) {
suspend fun login(loginModel: LoginModel) {
GlobalScope.launch {
val response = meloApi.makeLogin(loginModel)
response.isSuccessful
val userName = response.body()?.userName
val password = response.body()?.password
loginRequest(userName, password)
}
}
private suspend fun loginRequest(userName: String?, password: String?){
}
}
If you want to store as object you have to use serialization/deserialization.
I used gson here.
val loginResponse = LoginResponse("AToken", "RToken", "200", User("Edgar"))
val objToString = Gson().toJson(loginResponse)
val sharedPref = context.getSharedPreferences("loginRes", MODE_PRIVATE)
sharedPref.edit().putString("response", objToString).apply()
val response = sharedPref.getString("response", "null")
val stringToObj = Gson().fromJson(response, LoginResponse::class.java)
You can use proto datastore instead of shared preferences.

Koin DI Clear dependencies

I am passing the token to the GET method of the request using Koin. But after authorizing a new user, the old token is saved. To get a new access token, you need to exit the application, log in again and log in.
How do I get the Koin dependencies to be cleared when the Logout button is clicked?
val appModule = module {
factory { provideToken(provideSharedPreferences(androidContext())) }
}
private fun provideSharedPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("token", Context.MODE_PRIVATE)
fun provideToken(sharedPreferences: SharedPreferences): String =
sharedPreferences.getString("key", "")
Inject token:
class VkRetrofitDataSource (
private val vkRetrofitApi: VkRetrofitApi,
private val ioDispatcher: CoroutineDispatcher,
) : VkNetworkDataSource, KoinComponent {
private val accessToken: String by inject()
override suspend fun getUserInfo(
fields: String,
apiVersion: String
): Result<ResponseResultUser> =
withContext(ioDispatcher) {
val response = vkRetrofitApi.getUserInfo(fields, apiVersion, accessToken)
val userResponse = response.body()
Timber.d("Token $userResponse")
return#withContext if (response.isSuccessful && userResponse != null) {
Result.Success(userResponse)
}
}
I think what you need is
private val accessToken: String get() = get()
So, every time you access this property, it will invoke that factory in Koin module

Retrofit POST response conversion fails without a trace

I'm building an app for a company using MVVM & clean architecture so I've created 3 modules, the app module (presentation layer), the data module (data layer) & the domain module (domain/interactors layer). Now, in my data module, I'm using Retrofit and Gson to automatically convert the JSON I'm receiving from a login POST request to my kotlin data class named NetUserSession that you see below. The problem I'm having is that the logging interceptor prints the response with the data in it normally but the response.body() returns an empty NetUserSession object with null values which makes me think that the automatic conversion isn't happening for some reason. Can somebody please tell me what I'm doing wrong here?
KoinModules:
val domainModule = module {
single<LoginRepository> {LoginRepositoryImpl(get())}
single { LoginUseCase(get()) }
}
val presentationModule = module {
viewModel { LoginViewModel(get(),get()) }
}
val dataModule = module {
single { ApiServiceImpl().getApiService() }
single { LoginRepositoryImpl(get()) }
}
}
Api interface & retrofit:
interface ApiService {
#POST("Login")
fun getLoginResult(#Body netUser: NetUser) : Call<NetUserSession>
#GET("Books")
fun getBooks(#Header("Authorization") token:String) : Call<List<NetBook>>
}
class ApiServiceImpl {
fun getApiService(): ApiService {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
//TODO:SP Remove the interceptor code when done debugging
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
val retrofit = Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
// tell retrofit to implement the interface of our api
return retrofit.create(ApiService::class.java)
}
}
NetUserSession:
data class NetUserSession(
#SerializedName("expires_in")
val expires_in: Int,
#SerializedName("token_type")
val token_type: String,
#SerializedName("refresh_token")
val refresh_token: String,
#SerializedName("access_token")
val access_token: String
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type,
refreshToken = refresh_token,
accessToken = access_token
)
}
UserSession in domain:
data class UserSession(
val expiresIn:Int,
val tokenType:String,
val refreshToken:String,
val accessToken:String
)
LoginRepositoryImpl where the error occurs:
class LoginRepositoryImpl(private val apiService: ApiService) : LoginRepository {
override suspend fun login(username:String,password:String): UserSession? = withContext(Dispatchers.IO){
val response = apiService.getLoginResult(NetUser(username,password)).awaitResponse()
println("THE RESPONSE WAS : ${response.body()}")
return#withContext if(response.isSuccessful) response.body()?.toUserSession() else null
}
}
LoggingInterceptor result after the 200-OK:
{"expires_in":3600,"token_type":"Bearer","refresh_token":"T1amGR21.IdKM.5ecbf91162691e15913582bf2662e0","access_token":"T1amGT21.Idup.298885bf38e99053dca3434eb59c6aa"}
Response.body() print result:
THE RESPONSE WAS : NetUserSession(expires_in=0, token_type=null, refresh_token=null, access_token=null)
Any ideas what I'm failing to see here?
After busting my head for hours, the solution was to simply change the model class's members from val to var like so :
data class NetUserSession(
#SerializedName("expires_in")
var expires_in: Int = 0,
#SerializedName("token_type")
var token_type: String? = null,
#SerializedName("refresh_token")
var refresh_token: String? = null,
#SerializedName("access_token")
var access_token: String? = null
) {
fun toUserSession(): UserSession = UserSession(
expiresIn = expires_in,
tokenType = token_type!!,
refreshToken = refresh_token!!,
accessToken = access_token!!
)
}

Having trouble with initialising sharedPreferences in object

I am quite new to Kotlin & Android Studio.
I have a object RetrofitClient which I am trying to import sharedpreferences for URL, username etc
However I am struggling with getting it to work
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private val AUTH = "Basic " + Base64.encodeToString("A1122334".toByteArray(),Base64.NO_WRAP)
private const val BASE_URL = "http://192.168.2.5:3001/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
val requestBuilder = original.newBuilder()
.addHeader("Authorization", AUTH)
.method(original.method(),original.body())
val request = requestBuilder.build()
chain.proceed(request)
}.build()
val httpinstance: httpApi by lazy{
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
retrofit.create(httpApi::class.java)
}
}
Saved Preferences Class:
import android.content.Context
class LoginSavedPreferences(context: Context) {
private val sharedPreferences = context.getSharedPreferences("LoginPreferences",0)
// Save Boolean
fun saveBooleanPref(key: String, value: Boolean) {
sharedPreferences.edit().putBoolean(key,value).apply()
}
// get Boolean
fun getBooleanPref(key: String): Boolean {
return sharedPreferences.getBoolean(key,false)
}
// Save String
fun saveStringPref(key: String, value: String) {
sharedPreferences.edit().putString(key,value).apply()
}
// get String
fun getStringPref(key: String): String? {
return sharedPreferences.getString(key,null)
}
// Save Int
fun saveIntPref(key: String, value: Int) {
sharedPreferences.edit().putInt(key,value).apply()
}
// get Int
fun getIntPref(key: String): Int {
return sharedPreferences.getInt(key,0)
}
}
Here is a snippet from my LoginActivity, which works, but when I past the two lines into the RetrofitClient it will not initialse
class LoginActivity : AppCompatActivity() {
private lateinit var loginSavedPreferences: LoginSavedPreferences /// THIS LINE
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginSavedPreferences = LoginSavedPreferences(applicationContext) /// AND THIS LINE
// put values in the text boxes if we have them
val savedserveraddress = loginSavedPreferences.getStringPref(LoginConstants.KEY_SERVERADDRESS)
val savedusername = loginSavedPreferences.getStringPref(LoginConstants.KEY_USERNAME)
val savedpassword = loginSavedPreferences.getStringPref(LoginConstants.KEY_PASSWORD)
Try to change the lateinit var loginSavedPreferences to:
private val loginSavedPreferences: LoginSavedPreferences by lazy {
LoginSavedPreferences(this#LoginActivity)
}
and check what value you get in savedserveraddress with a breakpoint. If it still doesn't work try to change mode in getSharedPreferences to 1

Kotlin Json parsing MVVM

I'm trying to learn MVVM architecture on parse Json into a Recyclerview in MVVM using coroutines. But I'm getting error on BlogRepository class.
My Json file looks like this:
[
{
"id": 1,
"name": "potter",
"img": "https://images.example.com/potter.jpg"
},
{ …}
]
I have created data class as below:
#JsonClass(generateAdapter = true)
class ABCCharacters (
#Json(name = "id") val char_id: Int,
#Json(name = "name") val name: String? = null,
#Json(name = "img") val img: String
)
Then the RestApiService as below:
interface RestApiService {
#GET("/api")
fun getPopularBlog(): Deferred<List<ABCCharacters>>
companion object {
fun createCorService(): RestApiService {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl("https://example.com")
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build().create(RestApiService::class.java)
}
}
}
BlogReposity.kt
class BlogRepository() {
private var character = mutableListOf<ABCCharacters>()
private var mutableLiveData = MutableLiveData<List<ABCCharacters>>()
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
private val thisApiCorService by lazy {
RestApiService.createCorService()
}
fun getMutableLiveData():MutableLiveData<List<ABCCharacters>> {
coroutineScope.launch {
val request = thisApiCorService.getPopularBlog()
withContext(Dispatchers.Main) {
try {
val response = request.await()
val mBlogWrapper = response;
if (mBlogWrapper != null && mBlogWrapper.name != null) {
character = mBlogWrapper.name as MutableList<ABCCharacters>
mutableLiveData.value=character;
}
} catch (e: HttpException) {
// Log exception //
} catch (e: Throwable) {
// Log error //)
}
}
}
return mutableLiveData;
}
}
Finally the ViewModel class
class MainViewModel() : ViewModel() {
val characterRepository= BlogRepository()
val allBlog: LiveData<List<ABCCharacters>> get() = characterRepository.getMutableLiveData()
override fun onCleared() {
super.onCleared()
characterRepository.completableJob.cancel()
}
}
I've done this based on https://itnext.io/kotlin-wrapping-your-head-around-livedata-mutablelivedata-coroutine-networking-and-viewmodel-b552c3a74eec
Someone can guide me where am I going wrong & how to fix it?
Your getPopularBlog() API return List<ABCCharacters> instead of ABCCharacters. So, you can't access ABCCharacters's property from your response directly. That's why name property here shows Unresolved reference.
Try below code:
if (mBlogWrapper != null && mBlogWrapper.isNotEmpty()) {
character = mBlogWrapper as MutableList<ABCCharacters>
mutableLiveData.value = character
}
Your response return List but you want to check single object value (mBlogWrapper.name != null). You dont need this line on your code. In the example he checked if "response" is not a "null" and if the blog list is not null. Analize example one more time ;)
if you still have a problem with it, let me know

Categories

Resources