I am trying to retrieve data from an api using retrofit. The request am going to use needs an access token to pass in the header. The problem is that the token expires after 10 min and the refresh token request needs an unexpired token to create a new one!
So what should i do to keep the token refreshed by it self before passing 10 min?
I already tried Interceptor but it can't work with this type of problem because i need a valid token to get a new one
You can use a Worker and set it to run every 30min or so and set it to save the renewed token in your SharedPreference
here's an example for the Worker
class UpdateTokenWorkManger(
val context: Context,
params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
LoginHandler.refreshTokenSilently()
// Indicate whether the work finished successfully with the Result
return Result.success()
}
companion object {
private const val TAG = "Token Refresh "
const val TOKEN_REFRESH_WORK_MANGER_ID = "automatic_renew_token_work_manger"
fun renewToken() {
val periodicRefreshRequest = PeriodicWorkRequest.Builder(
UpdateTokenWorkManger::class.java, // Your worker class
30, // repeating interval
TimeUnit.MINUTES
)
val periodicWorkRequest: PeriodicWorkRequest = periodicRefreshRequest
.build()
WorkManager.getInstance(App.getApplication()).enqueueUniquePeriodicWork(
TOKEN_REFRESH_WORK_MANGER_ID,
ExistingPeriodicWorkPolicy.REPLACE,
periodicWorkRequest
)
}
}
to use this component you will need these dependencies
implementation "androidx.work:work-runtime-ktx:2.4.0"
also note that LoginHandler is the class that should be responsible for handling your login, refresh and logout scenarios.
and don't forget to add this line to your first Activity after the login Activity, for example: if you login in SplashActivity and after succesful authentication you redirect to MainActivity, then this line should be in MainActivity's onCreate function
UpdateTokenWorkManger.renewToken()
Related
Sorry for the verbose title.
I have a class that extend's okHttp's Authenticator interface where I need inside the overriden authenticate() to make an asynchronous network call to get a refresh token, but I must save the result to shared preferences immediately, so I have to block using runBlocking, which I've seen numerous people claiming it shouldn't be used in production code, but it's the only way to get it working for me.
Any ideas if there's a better way to achieve this, or is this an acceptable usage of runBlocking?
class RefreshTokenAuthenticator #Inject constructor(
private val serviceApi: ServiceApi,
private val sharedPreferences: SharedPreferences
) : Authenticator {
override fun authenticate(route: Route?, response:
Response): Request? {
if (response.request.url.toString().contains("/refresh")) {
return null
}
runBlocking(Dispatchers.IO) {
val apiResponse = serviceApi.get().refreshToken()
sharedPreferences.saveCredentials(apiResponse)
}.let { isRefreshed ->
if (isRefreshed) {
val newToken = sharedPreferences.getToken().orEmpty()
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
} else {
return null
}
}
}
}
In my opinion, first you should encapsulate serverApi and sharedPreferences by using the Repository pattern then use WorkManager and schedule a repeated work that requests the server for a new refresh token and stores it encrypted in SharedPreferences (I preferred DataStore) then get saved refresh token from Repository and use in RefreshTokenAuthenticator.
In my app I start a WebSocketWorker tasks that runs periodically every 15 minutes. As the name implies, it contains a WebSocket for listening to a socket in the background:
// MainApplication.kt
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
val work = PeriodicWorkRequestBuilder<WebSocketWorker>(15, TimeUnit.MINUTES).build()
workManager.enqueueUniquePeriodicWork("UniqueWebSocketWorker", ExistingPeriodicWorkPolicy.KEEP, work)
}
The WebSocketWorker contains the following logic:
#HiltWorker
class WebSocketWorker #AssistedInject constructor(
#Assisted appContext: Context,
#Assisted workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
inner class MyWebSocketListener : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
Timber.d("The message sent is %s", text)
// do sth. with the message
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
t.localizedMessage?.let { Timber.e("onFailure: %s", it) }
response?.message?.let { Timber.e("onFailure: %s", it) }
}
}
override suspend fun doWork(): Result {
try{
// code to be executed
val request = Request.Builder().url("ws://***.***.**.***:8000/ws/chat/lobby/").build()
val myWebSocketListener = MyWebSocketListener()
val client = OkHttpClient()
client.newWebSocket(request, myWebSocketListener)
return Result.success()
}
catch (throwable:Throwable){
Timber.e("There is a failure")
Timber.e("throwable.localizedMessage: %s", throwable.localizedMessage)
// clean up and log
return Result.failure()
}
}
}
As you can see, in the Worker class I set the WebSocket and everything is fine. Listening to the socket works.
Now, I also want to add the "sending of messages" functionality to my app. How can I reuse the websocket created in WebSocketWorker? Can I pass input data to the WebSocketWorker that runs in the background ?
Let's say I have a EditText for typing the message and a Button to send the message with a setOnClickListener attached like this:
binding.sendButton.setOnClickListener {
// get message
val message = binding.chatMessageEditText.text.toString()
// check if not empty
if(message.isNotEmpty()) {
// HOW CAN I REUSE THE WEBSOCKET RUNNING PERIODICALLY IN THE BACKGROUND?
// CAN I PASS THE MESSAGE TO THAT WEBSOCKET ?
// OR SHOULD I CREATE A DIFFERENT WORKER FOR SENDING MESSAGES (e.g.: a OneTimeRequest<SendMessageWorker> for sending messages ?
}
}
From the documentation, I know that you need to build Data objects for passing inputs and so on but there was no example which showcased how to pass input to a worker running periodically in the background.
My experience is saying that you can. Basically you "can't" interact with the worker object via the API. It is really annoying.
For example, with the JS you have the option to get a job and check the parameters of the job. There is no such option with the work. For example, I want to check what is the current state of the restrictions - what is satisfied, what is not. Nothing like this. You can just check states, cancel and that is almost all.
My suggestions is that it is because the WorkManager is a "facade/adapter" over other libraries like JS. It has it's own DB to restore JS jobs on device restart and stuff like this, but beside that if you want to interact with the internals I guess it was just too complicated for them to do so they just skipped.
You can just inject some other object and every time the work can ask it for it's data. I don't see other option.
I'm using stateFlow in my viewModel to get the result of api calls with a sealed class like this :
sealed class Resource<out T> {
data class Loading<T>(val data: T?): Resource<T>()
data class Success<T>(val data: T?): Resource<T>()
data class Error<T>(val error: Exception, val data: T?, val time: Long = System.currentTimeMillis()): Resource<Nothing>()
}
class VehicleViewModel #ViewModelInject constructor(application: Application, private val vehicleRepository: VehicleRepository): BaseViewModel(application) {
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
}
I would like to propose in my UI a button so that if the api call fails, the user can retry the call. I know there is a retry method on flows but it can not be called manually as it is only triggered when an exception occurs.
A common use case would be: The user have no internet connection, when the api call returns a network exception, I show a message to the user telling him to check its connection, then with a retry button (or by detecting that the device is now connected, whatever), I retry the flow.
But I can't figure a way to do it as you can not, like call flow.retry(). The actual retry method will be called as soon as an exception occurs. I don't want to retry immediately without asking the user to check it's connection, it wouldn't make sense.
Actually the only solution I found is to recreate the activity when the retry button is pressed so the flow will be reseted, but it's terrible for performances of course.
Without flows the solution is simple and there is plenties of examples , you just have to relaunch the job, but I can't find a way to do it properly with flows. I have logic in my repository between the local room database and the remote service and the flow api is really nice so I would like to use it for this use case too.
I recommend keep the vehicleResource as a field in viewmodel and call a function to make an API call to fetch data.
private val _vehiclesResource = MutableStateFlow<Resource<List<Vehicle>>>(Resource.Loading(emptyList()))
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = _vehiclesResource.asStateFlow()
init {
fetchVehicles()
}
private var vehicleJob : Job? = null
fun fetchVehicles() {
vehicleJob?.cancel()
vehicleJob = viewModelScope.launch {
vehicleRepository.getVehicles().collect {
_vehiclesResource.value = it
}
}
}
The API will be called in the constructor of viewmodel. And also you can call this function via the view (button's click listener) for error state.
P.S: I should mention that this line of your code has issue and SharedFlow couldn't be cast to StateFlow.
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
I have integration with backend made with retrofit.
My project architecture is similar to known GithubBrowserSample proposed by Google as example.
This means, my ViewModel has some LiveData that can be observed
Any request can throw "401 unauthorized" and I would like to redirect to login fragment.
And I can do this when checking details of errors at fragment during observing
viewModel.user.observe(viewLifecycleOwner, { response ->
if (response.status == Status.SUCCESS) {
// do work there
} else if (response.status == Status.ERROR) {
// check does this 401 and redirect
}
})
But, I have a lot of such places in my app and I need some mechanism
for listening of 401 and redirect which can be applied to all places out of the box without checking in every place
Let's say, some wrapper, some utils to reuse check for specific error
Make a Base class , Create a class which extends the AppCompactActivity means the base class will be a Parent activity and listen for the user.observe there and extends your current activity with the parent activity :
class parentActivity extends AppCompactActivity() {
onCreate(){
////Listener for the observer here
this.user.obser{
}
}
}
//////////////////////////
class yourActivity extends parentActivity(){
//TODO your code
}
This might not limit the checking you need to do, but it moves the logic from the Activity into the ViewModel (where it can be easier tested).
In your ViewModel
val user: LiveData<Result<User>> = repository.getUser() // put your actual source of fetching the user here
val needsRedirectToLogin: LiveData<Boolean> = Transformations.map(user) { response ->
response.code == 401
}
Improvement: You could create an extension function for this, you can reuse in other ViewModels for the same scenario:
/**
* Returns true when [code] matches the Response code.
*/
fun LiveData<Response<Any>>.isErrorCode(code: Int): LiveData<Boolean> = Transformations.map(this) { response ->
response.code == code
}
And then use it like:
val needsRedirectToLogin: LiveData<Boolean> = user.isErrorCode(401)
In your Fragment:
viewModel.needsRedirectToLogin.observe(viewLifecycleOwner, { redirectToLoginPage ->
if (redirectToLoginPage) {
// do the redirect here.
}
The next step could be having an EventBus, or BroadcastReceiver you could send this event. So that you have only one place to handle this.
There are lots of ways how to go from here (depending on your requirements). But I hope this gives you an idea and will help you.
If the error you want to handle is specific to 401 Unauthorized, you can implement Authenticator and attach it to your okhttp.
First, create your Authenticator class
class YourAuthenticator : Authenticator {
// This callback is only called when you got 401 Unauthorized response
override fun authenticate(route: Route?, response: Response): Request? {
// Navigate to Login fragment
}
}
Then, add Authenticator to your okhttp builder
val yourAuthenticator = YourAuthenticator()
val okHttp = OkHttpClient.Builder()
.authenticator(yourAuthenticator)
.build()
I'm doing a REST API call with Ion in order to log in a user. In order to do that, I'm getting the accessToken that Ion returns me (and this comes asynchronously). Initially, I was doing everything in the activity and inside the callback (I was getting the accessToken and then starting a new activity intent while sending this token to the next activity). It was working fine like that.
Now, I'm refactoring my project to an MVP architecture, so I've split the Ion network call to a different class (in the model layer level of MVP). However, I have a problem. It always returns me null and I suspect it's because the function returns before Ion finishes its async task. Any idea?
Here is the function that I want to get the loginSession from:
override fun userAuth(username: String,
password: String,
context: Context): LoginSession? {
var loginSession: LoginSession? = null
Ion.with(context)
.load("https://myURL")
.setBodyParameter("UserName", username)
.setBodyParameter("Password", password)
.asString()
.setCallback { e, result ->
try {
val json = JSONObject(result)
val expiresIn = json.getInt("expires_in")
val tokenType = json.getString("token_type")
val refreshToken = json.getString("refresh_token")
val accessToken = json.getString("access_token")
loginSession = LoginSession(expiresIn, tokenType, refreshToken, accessToken)
} catch (jsonException: JSONException) {
jsonException.printStackTrace()
}
}
return loginSession
}
creates an interface, make the class where you wanna callback implements it.once you are done with the network call inside your model.
Use the method of that interface to get a callback in your activity/fragment.
You must have a reference of that interface in your model which must have been initialized with your activity/fragment object before your network calls start to process.
Note:- Your network call must be in Presenter in MVP and not in model.Retrofit already does all this more easily what i explained.You may use Retrofit instead of Ion.