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
Related
I want to pass header authorization token in HTTP requests in retrofit. The token is saved in DataStore. My problem is how to retrieve the token from DataStore and pass it to the intercepter header. I've spent hours thinking of a workaround and searching through the internet but I came with nothing. I'm also new to Kotlin. Here is the code snippet:
interface RoomAPIService {
#GET("rooms")
fun getAllRooms(#Header("Authorization") authHeader: String): Call<List<Room>>
var context: Context
companion object {
var retrofitService: RoomAPIService? = null
var token: String = ""
fun getInstance() : RoomAPIService {
GlobalScope.launch(Dispatchers.IO)
{
//How to pass context to DataRepository.getInstance(context)
token = DataStoreRepository.getInstance().getToken().toString()
}
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor { chain ->
val request = chain.request().newBuilder().addHeader("Authorization","Bearer " + theTokenRetrievedFromDataStore).build()
chain.proceed(request)
}
.
.
}
.
.
}
Here is DataStoreRepository.kt:
class DataStoreRepository(context: Context) {
private val dataStore: DataStore<Preferences> = context.createDataStore(
name = "token_store"
)
companion object {
private val TOKEN = preferencesKey<String>("TOKEN")
private var instance: DataStoreRepository? = null
fun getInstance(context: Context): DataStoreRepository {
return instance ?: synchronized(this) {
instance ?: DataStoreRepository(context).also { instance = it }
}
}
}
suspend fun savetoDataStore(token: String) {
dataStore.edit {
it[TOKEN] = token
}
}
suspend fun getToken(): String? {
val preferences: Preferences = dataStore.data.first()
Log.d("datastore", "token retrieved: ${preferences[TOKEN]} +++++++++++")
return preferences[TOKEN]
}
}
And here is MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var logoutBtn: Button
private lateinit var bottomNavigation: BottomNavigationView
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: RoomViewModel
private val retrofitService = RoomAPIService.getInstance(this)
val adapter = RoomsAdapter()
private var token: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
logoutBtn = binding.logoutBtn
bottomNavigation = binding.bottomNavigation
viewModel = ViewModelProvider(this, RoomViewModeFactory(RoomRepository(retrofitService))).get(RoomViewModel::class.java)
binding.recyclerview.adapter = adapter
var token = intent.getStringExtra("token")
Log.d("tokenCheck","checkToken: $token")
if (token != null) {
viewModel.getAllRooms(token)
}
..
Any help will be greatly appreciated!
You can change your getInstance() signature to contain Context object.
fun getInstance(context:Context) : RoomAPIService {
GlobalScope.launch(Dispatchers.IO)
{
token = DataRepository.getInstance(context).getToken().toString()
}
}
This is not a good practise to use "context" in a non-ui module in your application. Networking modules ( or Data layer you may say) should not know about android libraries and components. My suggestion is to use a value in an "variable" which can be changed in runtime and can be seen in the Network module then use it in the OkHttp interceptor. Finally, you just need to initialize the variable at the beginning of your application.
I want to use coroutines in my project only when I use coroutines I get the error :Unable to invoke no-args constructor. I don't know why it's given this error. I am also new to coroutines.
here is my apiclient class:
class ApiClient {
val retro = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Here is my endpoint class:
#GET("v2/venues/search")
suspend fun get(
#Query("near") city: String,
#Query("limit") limit: String = Constants.limit,
#Query("radius") radius: String = Constants.radius,
#Query("client_id") id: String = Constants.clientId,
#Query("client_secret") secret: String = Constants.clientSecret,
#Query("v") date: String
): Call<VenuesMainResponse>
my Repository class:
class VenuesRepository() {
private val _data: MutableLiveData<VenuesMainResponse?> = MutableLiveData(null)
val data: LiveData<VenuesMainResponse?> get() = _data
suspend fun fetch(city: String, date: String) {
val retrofit = ApiClient()
val api = retrofit.retro.create(VenuesEndpoint::class.java)
api.get(
city = city,
date = date
).enqueue(object : Callback<VenuesMainResponse>{
override fun onResponse(call: Call<VenuesMainResponse>, response: Response<VenuesMainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<VenuesMainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
my ViewModel class:
class VenueViewModel( ) : ViewModel() {
private val repository = VenuesRepository()
fun getData(city: String, date: String): LiveData<VenuesMainResponse?> {
viewModelScope.launch {
try {
repository.fetch(city, date)
} catch (e: Exception) {
Log.d("Hallo", "Exception: " + e.message)
}
}
return repository.data
}
}
part of activity class:
class MainActivity : AppCompatActivity(){
private lateinit var venuesViewModel: VenueViewModel
private lateinit var adapter: HomeAdapter
private var searchData: List<Venue>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.main_search)
venuesViewModel = ViewModelProvider(this)[VenueViewModel::class.java]
venuesViewModel.getData(
city = "",
date = ""
).observe(this, Observer {
it?.let { res ->
initAdapter()
rv_home.visibility = View.VISIBLE
adapter.setData(it.response.venues)
searchData = it.response.venues
println(it.response.venues)
}
})
this is my VenuesMainResponse data class
data class VenuesMainResponse(
val response: VenuesResponse
)
I think the no-args constructor warning should be related to your VenuesMainResponse, is it a data class? You should add the code for it as well and the complete Log details
Also, with Coroutines you should the change return value of the get() from Call<VenuesMainResponse> to VenuesMainResponse. You can then use a try-catch block to get the value instead of using enqueue on the Call.
Check this answer for knowing about it and feel free to ask if this doesn't solve the issue yet :)
UPDATE
Ok so I just noticed that it seems that you are trying to use the foursquare API. I recently helped out someone on StackOverFlow with the foursquare API so I kinda recognize those Query parameters and the Venue response in the code you provided above.
I guided the person on how to fetch the Venues from the Response using the MVVM architecture as well. You can find the complete code for getting the response after the UPDATE block in the answer here.
This answer by me has code with detailed explanation for ViewModel, Repository, MainActivity, and all the Model classes that you will need for fetching Venues from the foursquare API.
Let me know if you are unable to understand it, I'll help you out! :)
RE: UPDATE
So here is the change that will allow you to use this code with Coroutines as well.
Repository.kt
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
suspend fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
try {
val response = api.get(
longLat = longlat,
date = date
)
_data.value = response
} catch (e: Exception) {
_data.value = null
}
}
}
ViewModel.kt
class ViewModel : ViewModel() {
private val repository = Repository()
val data: LiveData<mainResponse?> = repository.data
fun getData(longLat: String, date: String) {
viewModelScope.launch {
repository.fetch(longLat, date)
}
}
}
api.kt
interface api {
#GET("v2/venues/search")
suspend fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): mainResponse
}
MainActivity.kt
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74",
date = "20210718" // date format is: YYYYMMDD
)
viewModel.data
.observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
Log.d("name ",name)
}
}
})
}
}
Good day all, am trying to test my ViewModel class and it has a dependency of datasource, I tried to mock this, but it won't work because it's an interface, I believe the interface implementation is generated at runtime, how do I unit test this class, below is my ViewModel class
class LoginViewModel #ViewModelInject constructor(#ApplicationContext private val context: Context,
private val networkApi: NetworkAPI,
private val dataStore: DataStore<Preferences>)
: ViewModel() {
val clientNumber = MutableLiveData<String>()
val clientPassword = MutableLiveData<String>()
private val _shouldNavigate = MutableLiveData(false)
val shouldNavigate: LiveData<Boolean>
get() = _shouldNavigate
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String>
get() = _errorMessage
private val _activateDeviceButton = MutableLiveData(false)
val activateButton : LiveData<Boolean>
get() = _activateDeviceButton
init {
populateApiWithFakeData()
}
suspend fun authenticateUsers(): Boolean {
val clientNumber = clientNumber.value
val clientPassword = clientPassword.value
requireNotNull(clientNumber)
requireNotNull(clientPassword)
val (userExist, token) = networkApi.doesUserExist(clientNumber.toLong(), clientPassword)
if (token.isNotBlank()) storeTokenInStore(token)
return if (userExist) {
true
} else {
_errorMessage.value = "Incorrect account details. Please try again with correct details"
false
}
}
private suspend fun storeTokenInStore(token: String) {
dataStore.edit { pref ->
pref[TOKEN_PREFERENCE] = token
}
}
and here is my ViewModel Test class
#Config(sdk = [Build.VERSION_CODES.O_MR1])
#RunWith(AndroidJUnit4::class)
class LoginViewModelTest{
private val context : Context = ApplicationProvider.getApplicationContext()
private val dataCentre = NetworkApImpl()
#Mock
private lateinit var dataStore: DataStore<Preferences>
#Before
fun setUpDataCenters(){
val loginData = DataFactory.generateLoginData()
for (data in loginData){
dataCentre.saveUserData(data)
}
}
#After
fun tearDownDataCenter(){
dataCentre.clearDataSet()
}
#Test
#ExperimentalCoroutinesApi
fun authenticateUser_shouldAuthenticateUsers(){
//Given
val viewModel = LoginViewModel(context, dataCentre, dataStore)
viewModel.clientNumber.value = "8055675745"
viewModel.clientPassword.value = "robin"
//When
var result : Boolean? = null
runBlocking {
result = viewModel.authenticateUsers()
}
//Then
Truth.assertThat(result).isTrue()
}
Any assistance rendered will be appreciated.
You can wrap your dependency in a class you own as Mockito suggests here. This also has the upside of letting you change your storage implementation latter without having and impact on every view model using it.
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()
}
i've facing a problem to test sharedpreference in datastore. in actual datastore i implement three arguments, those include sharedpreference.
in this case i want to store value, and get that value. mocking not help here.
mocking cannot propagate actual value, that will be used by code. in second part.
class FooDataStoreTest : Spek({
given("a foo data store") {
val schedulerRule = TestSchedulerRule()
val service: FooRestService = mock()
val context: Context = mock()
val gson: Gson = mock()
val appFooPreference: SharedPreferences = mock()
var appFooSessionStoreService: AppFooSessionStoreService? = null
var fooStoredLocationService: FooStoredLocationService? = null
beforeEachTest {
appFooSessionStoreService = AppFooSessionStoreService.Builder()
.context(context)
.gson(gson)
.preference(appFooPreference)
.build()
fooStoredLocationService = FooStoredLocationService(appFooSessionStoreService)
}
val longitude = 106.803090
val latitude = -6.244285
on("should get foo service with request longitude $longitude and latitude $latitude") {
it("should return success") {
with(fooStoredLocationService) {
val location = Location()
location.latitude = latitude
location.longitude = longitude
// i want to store location in this
fooStoredLocationService?.saveLastKnownLocation(location)
// and retrieve in below code
val l = fooStoredLocationService?.lastKnownLocation
val dataStore = FooDataStore(service, preference, fooStoredLocationService!!)
service.getFooService(longitude, longitude) willReturnJust
load(FooResponse::class.java, "foo_response.json")
val testObserver = dataStore.getFooService().test()
schedulerRule.testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
testObserver.assertNoErrors()
testObserver.awaitTerminalEvent()
testObserver.assertComplete()
testObserver.assertValue { actual ->
actual == load(FooResponse::class.java, "foo_response.json")
}
}
}
}
afterEachTest {
appFooSessionStoreService?.clear()
fooStoredLocationService?.clear()
}
}})
and this datastore looks like
open class FooDataStore #Inject constructor(private val fooRestService: FooRestService,
private val fooPreference: FooPreference,
private val fooLocation: fooStoredLocationService) : FooRepository {
private val serviceLocation by lazy {
fooLocation.lastKnownLocation
}
override fun getFooService(): Single<FooResponse> {
safeWith(serviceLocation, {
return getFooLocal(it).flatMap({ (code, message, data) ->
if (data != null) {
Single.just(FooResponse(code, message, data))
} else {
restService.getFooService(it.longitude, it.latitude).compose(singleIo())
}
})
})
return Single.error(httpExceptionFactory(GPS_NOT_SATISFYING))
}
}
Actually i want to get value in from this field serviceLocation. Anyone has approach to do some test for that?, any advise very welcome.
thanks!
I would recommend you not to depend on SharedPreferences directly, but to have some interface LocalStorage, so you can have your SharedPrefsLocalStorage being used in the code and TestLocalStorage in the tests. SharedPrefsLocalStorage will use SharedPreferences under the hood, and TestLocalStorage some Map implementation.
Just a simple example:
// You may add other type, not Int only, or use the String and convert everything to String and back
interface LocalStorage {
fun save(key: String, value: Int)
fun get(key: String): Int?
}
class SharedPrefsLocalStorage(val prefs: SharedPreferences) : LocalStorage {
override fun save(key: String, value: Int) {
with(prefs.edit()){
putInt(key, value)
commit()
}
}
override fun get(key: String): Int? = prefs.getInteger(key)
}
class TestLocalStorage : LocalStorage {
val values = mutableMapOf<String, Any>()
override fun save(key: String, value: Int) {
values[key] = value
}
override fun get(key: String): Int? = map[value] as Int?
}