Start by adding the following dependency to the app module’s build.gradle file:
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
This adds the logging interceptor to the project. Sync the project, and then navigate to
RetrofitConfig.kt to add the interceptor to the OkHttp instance. First, create an
interceptor by adding the following code:
private fun buildLoggingInterceptor() = HttpLoggingInterceptor()
.setLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE)
Note: If you can’t see
BuildConfig, make sure to build the project first.
You create a new instance of HttpLoggingInterceptor and set the logging level based on the DEBUG
flag. If it’s a DEBUG build, you set the logging level to BODY. This means that the
interceptor will log requests and responses together with their headers and bodies. If DEBUG
is false, you set the logging level to NONE. That’s because you don’t want to leak any data
in a live, production environment.
With the interceptor ready, add it to the OkHttp instance by replacing the existing buildClient function with this code:
private fun buildClient(): OkHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor(buildLoggingInterceptor())
.build()
By calling addNetworkInterceptor(buildLoggingInterceptor()), OkHttp automatically logs all
your requests and responses.
To see how this works, build and run the app and then navigate to the movies screen to trigger a request.
Open the Logcat and type package:mine tag:okhttp.OkHttpClient
in the filtering field to reduce clutter and keep only the networking logs.
You can find several useful pieces of information in the output:
- Your request’s full URL.
- The response’s status code.
- How long it took to get the response.
- The server name.
- Request and response headers.
- Request and response bodies.
Now, it’s time to implement posting movie reviews, getting your own reviews instead of the sample ones, and fetching the profile information. These actions require authentication, so go back to RetrofitConfig.kt and add the following code:
private fun buildAuthInterceptor() = Interceptor { chain ->
val newRequest = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${App.getUserToken()}")
.build()
chain.proceed(newRequest)
}
You create a new Interceptor instance that makes a copy of the original request by
calling newBuilder(). It then adds an Authorization header to the new request with the
token that you received at login, and builds a Request instance by calling build(). When
you have the new request ready, call chain.proceed(), passing in the new request, to get back
the response.
Now, add the new interceptor to the OkHttp instance.
private fun buildClient(): OkHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor(buildAuthInterceptor())
.addNetworkInterceptor(buildLoggingInterceptor())
.build()
You must add the authentication interceptor before the logging interceptor, because the logging interceptor won’t log the new request otherwise.
With the authorization set up, you can implement the remaining functionalities. Go to
MovieDiaryApi.kt and implement postReview() and getProfile() like this:
suspend fun getProfile(): Result<User> = runCatching { apiService.getProfile() }
suspend fun postReview(movieReview: MovieReview): Result<MovieReview> =
runCatching { apiService.postReview(movieReview) }
This code should look familiar from the last lesson. You’re calling apiService methods
and wrapping them with runCatching.
To round everything up, go to MovieDiaryApiService.kt and change getMovies() to use the
original route instead of the one for fetching sample movies.
@GET("movies")
suspend fun getMovies(): List<MovieReview>
One last thing to do is to fix the UI code. Go to MoviesScreen.kt and add the following code:
NewEntryDialog(
onDismissRequest = { openDialog = false },
onConfirmation = { movieReview ->
screenScope.launch {
movieDiaryApi.postReview(movieReview)
.onSuccess { newReview ->
val newList = movieReviewList.toMutableList()
newList += newReview
movieReviewList = newList
}
.onFailure { scaffoldState.snackbarHostState.showSnackbar(it.message ?: "") }
}
openDialog = false
},
)
If a new review has been posted successfully, you create a copy of the list and add the new
review to it. If the request fails, you show a Snackbar as before.
ProfileScreen.kt also needs some adjustments. Add this code:
LaunchedEffect(Unit) {
movieDiaryApi.getProfile()
.onSuccess { user = it }
.onFailure { scaffoldState.snackbarHostState.showSnackbar(it.message ?: "") }
}
If getProfile() returns profile data, you simply assign it to the user state variable. If it
fails, show an error in Snackbar.
Build and run the app now. Try to add a review and check whether you can see your profile information by navigating to the profile page.
If you wait one minute and then try to make a new request, you’ll notice that the token has
expired and you get a 401 Unauthorized error. To avoid redirecting users to the login page,
you’ll create an Authenticator that will obtain a new token and retry the request.
Start by adding a new method in MovieDiaryApiService.kt that looks like this:
@POST("user/refreshToken")
suspend fun refreshToken(@Body refreshToken: Long): LoginResponse
You pass in refreshToken as a body parameter and get a new pair of tokens wrapped
in LoginResponse.
Now, go to MovieDiaryApi.kt and add the following code:
suspend fun refreshToken(refreshToken: Long): Result<LoginResponse> = runCatching {
apiService.refreshToken(refreshToken)
}
There’s nothing new here — you invoke the method you just created and wrap it with runCatching.
Now that you have the method for token refresh ready, it’s time to use it in the authenticator.
Go to the networking package and create a class called ApiAuthenticator. It
should look like this:
class ApiAuthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
runBlocking {
App.movieApi.refreshToken(App.getRefreshToken())
}.onSuccess {
App.saveUserToken(it.token)
return response.request.newBuilder()
.addHeader("Authorization", "Bearer ${it.token}")
.build()
}
return null
}
}
Note: You must import
Authenticator,Request,Response, andRoutefrom theokhttppackage.
Your class extends Authenticator, which is a class from the okhttp package. It overrides
authenticate(), which the OkHttp client invokes when the server responds with the status code
401. This method receives the response to the original request as a parameter and expects you
to return a new request with the credentials to satisfy the authentication challenge. In this
case, you fetch the token and attach it as an Authorization header. When you return the new
request, the client automatically executes it.
If the request fails, you return null to avoid retrying the request.
With the class ready, go to RetrofitConfig.kt and add it to the OkHttpClient instance by
adding the following:
private fun buildClient(): OkHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor(buildAuthInterceptor())
.addNetworkInterceptor(buildLoggingInterceptor())
.authenticator(ApiAuthenticator())
.build()
Doing this means the client creates a new request with the token added as an Authorization header and automatically retries for every request that fails with the status code 401.
Build and run the app again. Open the Logcat, and you’ll see that Retrofit will automatically retry the unauthorized request.