Network Requests with Retrofit in Android

Jun 5 2024 · Kotlin 1.9.22, Android 14, Android Studio Hedgehog | 2023.1.1

Lesson 06: Interceptors and Authentication

Demo

Episode complete

Play next episode

Next
Transcript

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, and Route from the okhttp package.

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.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion