I am trying to change the Retrofit baseUrl from SharedPreferences in my app at runtime, but the change is only implemented when I close and open the app. I have tried using onSharedPreferenceChangeListener() and onPreferenceChangeListener() but I still get the same result. How do I implement the listeners so that they change the baseUrl at runtime?
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(CompanyApiService .apiBaseUrl)
.build()
interface CompanyApiService {
#GET("employees")
fun getEmployeesAsync(): Deferred<List<Employees>>
#GET("title/{id}")
fun getTitlesAsync(#Path("id") id: Int): Deferred<List<Titles>>
#POST("message")
fun submitMessage(#Body message: Message): Call<String>
}
object CompanyApi {
val retrofitService: CompanyApiService by lazy {
retrofit.create(CompanyApiService ::class.java)
}
var apiBaseUrl = ""
}
MainActivity.kt
class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
...
PreferenceManager.setDefaultValues(this, R.xml.main_preference, false)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
sharedPrefs.registerOnSharedPreferenceChangeListener(this)
val apiBaseUrl = sharedPrefs.getString(KEY_PREF_BASE_URL, "")
CompanyApi.apiBaseUrl = apiBaseUrl!!
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == KEY_PREF_BASE_URL) {
val newApiBaseUrl = sharedPreferences?.getString(key, "")
CompanyApi.apiBaseUrl = newApiBaseUrl!!
}
}
object CompanyApi {
val retrofitService: CompanyApiService by lazy {
retrofit.create(CompanyApiService ::class.java)
}
This creates a singleton, you need to change that and re-create your Api when you changed your base_url however I wouldn't advise to do so. Creating a retrofit instance is consuming and you might get into errors later on.
Lucky for you Retrofit has a simple solution for that:
public interface UserManager {
#GET
public Call<ResponseBody> userName(#Url String url);
}
The URL String should specify the full Url you wish to use.
also, check this out -> enter link description here
Related
I'm creating an app where the user has to insert a serverurl in an EditText field, and that url should be the baseUrl of the retrofit-request.
So, my code works as it should when i use a hardcoded baseurl, but the app crashes when I try to pass the value from the Edittext to the baseUrl.
Thats how I tried to pass the value:
object NetworkLayer {
var newUrl: String = ""
val retrofit: Retrofit
get() = Retrofit.Builder()
.baseUrl(newUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
val myApi: MyApi by lazy {
retrofit.create(MyApi::class.java)
}
val apiClient = ApiClient(myApi)
}
and in my MainActivity:
var serverUrl = binding.et1.text.toString()
button.setOnClickListener {
NetworkLayer.newUrl = serverUrl
viewModel.getServerInformation(headerValue)
}
I get this error message: Error message: Caused by: java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but no scheme was found for.
So probably retrofit uses the empty "" string for the request. Somehow I should send the information to retrofit that when clicking the button the url from the Edittext (et1) is the baseUrl. When I use a seperate class (f.e. class Constants, with a companion object with a const val baseUrl = "hardcoded url") it works also.
Can I create a function to inform the retrofit client to use the Edittext as baseUrl and declare it in the onClickListener? or could it be a way to create the retrofit client in a class instead of an object? (using url: String as parameter in the class and adding the edittext as argument in the MainActivity?)
Sadly the #Url annotation for Retrofit doesn't work as I have to use also #Header and #Query in the different requests.
Or is there a compeletey different way for doing this?
Hopefully there is someone who can help me.
I managed to solve it, the only thing I had to change was:
val url = binding.etServerUrl.text instead of
val url = binding.etServerUrl.text.toString()
and when calling the function on button click I added the toString() to the url argument. When I try to add the toString() to the val url as I always did before it doesn't work, anyone can tell me why?
Here is an example how I use it (I changed the Retrofit client a bit to my first version in the question). So finally I can go ahead with my app, as I was blocked now for a few weeks with this.. :-)
object RetrofitClient{
var retrofitService: MyApi? = null
fun getInstance(url: String): MyApi{
if (retrofitService == null) {
val retrofit = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(MyApi::class.java)
}
return retrofitService!!
}
}
I changed the retrofitclient a bit, but it wors
Then in the repository:
class MainRepository (){
suspend fun getToken(cookie: String, url: String): TokenResponse? {
val request = RetrofitClient.getInstance(url).getToken(cookie)
if (request?.isSuccessful!!) {
return request.body()!!
}
return null
}
}
Viewmodel:
class SharedViewModel() : ViewModel() {
private val repository = MainRepository()
private val _getTokenLiveData = MutableLiveData<TokenResponse>()
val getTokenLiveData: LiveData<TokenResponse> = _getTokenLiveData
fun getToken(cookie: String, url: String) {
viewModelScope.launch {
val response = repository.getToken(cookie, url)
_getTokenLiveData.postValue(response)
}
}
}
And finally the MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val viewModel: SharedViewModel by lazy {
ViewModelProvider(this)[SharedViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) //initializing the binding class
setContentView(binding.root)
val url = binding.etServerUrl.text
val headerValue = binding.etMac.text.toString()
val button = binding.button
val textView = binding.textView
button.setOnClickListener {
viewModel.getToken(headerValue, url = url.toString())
}
viewModel.getTokenLiveData.observe(this) { response ->
if (response == null) {
Toast.makeText(this#MainActivity, "Fehlerhaft", Toast.LENGTH_SHORT).show()
return#observe
}
textView.text = response.js.token
}
}
}
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!!
)
}
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
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()
}
Here is my Retrofit Interface and creation code:
interface SSApi {
companion object {
private fun create(): SSApi {
val httpClient = OkHttpClient().newBuilder()
val networkInterceptor = Interceptor { chain ->
val request = chain.request()?.newBuilder()?.addHeader("api-key", SSConstants.API_KEY)?.build()
chain.proceed(request!!)
}
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient.addNetworkInterceptor(networkInterceptor).addInterceptor(loggingInterceptor)
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(SSConstants.BASE_URL)
.client(httpClient.build())
.build()
return retrofit.create(SSApi::class.java)
}
val api by lazy {
SSApi.create()
}
var disposable: Disposable? = null
}
#GET
fun getWeatherInfo(#Url url: String): Observable<OpenWeatherMapInfo>
}
And here is how I use the disposable:
private fun getWeather() {
disposable = api
.getWeatherInfo(SSConstants.OPEN_WEATHER_MAP_API_ENDPOINT)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ results -> Log.i("Dale", results.toString())},
{ error -> Log.i("Dale", error.message)}
)
}
When I execute the request, I can see that it my OPEN_WEATHER_MAP_API_ENDPOINT still appends to my baseUrl.
Here is my Constants class for reference:
object SSConstants {
const val OPEN_WEATHER_MAP_API_ENDPOINT = "api.openweathermap.org/data/2.5/weather?q=Catbalogan,PH&units=metric"
const val BASE_URL = "https://api.xxx.xxx/"
}
Your issue is that you didn't provide the full URL in the dynamic call, and that's why Retrofit is trying to make the call relative to the base url still. Just add https:// to the dynamic URL:
const val OPEN_WEATHER_MAP_API_ENDPOINT = "https://api.openweathermap.org/data/2.5/weather?q=Catbalogan,PH&units=metric"
Here is an article discussing how the dynamic URL is resolved in different scenarios, for further reference.