Network Requests with Retrofit in Android

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

Lesson 04: Parse JSON with Moshi

Demo 2

Episode complete

Play next episode

Next
Transcript

To start, open the app module’s build.gradle and add the dependency for the Moshi converter:

implementation "com.squareup.retrofit2:converter-moshi:2.9.0"

Sync the project and then navigate to RetrofitConfig.kt. You need to add the converter factory to the Retrofit builder. Do it with the following code:

.addConverterFactory(MoshiConverterFactory.create())

This tells Retrofit to use MoshiConverter for all API requests made with this Retrofit instance. With the converter ready, head over to MovieDiaryApiService.kt and change the existing methods like this:

@POST("user/register")
fun registerUser(@Body body: RegisterBody): Call<Unit>

@POST("user/login")
fun loginUser(@Body body: RegisterBody): Call<LoginResponse>

@GET("user")
fun getProfile(): Call<User>

@GET("sampleMovies")
fun getMovies(): Call<List<MovieReview>>

@POST("movies")
fun postReview(@Body movieReview: MovieReview): Call<MovieReview>

Instead of ResponseBody and RequestBody, you now use custom data types directly. LoginResponse is red because it doesn’t exist yet. To fix that, create a response package inside the model package, and add the LoginResponse class.

@JsonClass(generateAdapter = true)
data class LoginResponse(val token: String)

It’s a data class holding a token value, which the back end will return upon successful login. Now, import the class in MovieDiaryServiceApi.kt, and the error should be gone.

It’s time to change the implementations of the existing methods in MovieDiaryApi.kt to work with the new data types. First, you’ll fix registerUser(). Delete the code you added in the previous demo section and replace it with this:

val body = RegisterBody(username, password, email)

apiService.registerUser(body).enqueue(object : Callback<Unit> {
  override fun onResponse(call: Call<Unit>, response: Response<Unit>) {
    if (response.isSuccessful) {
      onResponse("Success", null)
    } else {
      onResponse(null, Throwable(response.message()))
    }
  }

  override fun onFailure(call: Call<Unit>, error: Throwable) {
    onResponse(null, error)
  }
})

You set the return type to Unit because you don’t expect any payload to come as a response. A simple check that the response is successful is sufficient for this request. The rest of the code is the same as before. You invoke the onResponse callback, passing in a message or an error if it occurs.

Change the other two methods similarly. Implement loginUser() like this:

fun loginUser(
  username: String,
  password: String,
  onResponse: (LoginResponse?, Throwable?) -> Unit
) {
  val loginBody = RegisterBody(username, password)

  apiService.loginUser(loginBody).enqueue(object : Callback<LoginResponse> {
    override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
      if (response.isSuccessful) {
        onResponse(response.body(), null)
      } else {
        onResponse(null, Throwable(response.errorBody()?.string()))
      }
    }

    override fun onFailure(call: Call<LoginResponse>, error: Throwable) {
      onResponse(null, error)
    }
  })
}

The onResponse callback now accepts LoginResponse instead of a String. You use the same RegisterBody class for the login body. email property is set to null by default, so it won’t be serialized to JSON. The rest of the code works the same as before.

The last method to refactor is getMovies. It should look like this:

fun getMovies(onResponse: (List<MovieReview>?, Throwable?) -> Unit) {
  apiService.getMovies().enqueue(object : Callback<List<MovieReview>> {
    override fun onResponse(call: Call<List<MovieReview>>, response: Response<List<MovieReview>>) {
      if (response.isSuccessful) {
        onResponse(response.body(), null)
      } else {
        onResponse(null, Throwable(response.errorBody()?.string()))
      }
    }

    override fun onFailure(call: Call<List<MovieReview>>, error: Throwable) {
      onResponse(null, error)
    }
  })
}

You still must make Moshi aware of MovieReview. Open MovieReview.kt and add the @JsonClass(generateAdapter = true) annotation to the class.

Before running the app, you must make two more changes in the UI layer. Open LoginScreen.kt and change the following:

@Composable
fun LoginScreen(
  movieDiaryApi: MovieDiaryApi,
  connectivityChecker: ConnectivityChecker,
  onLogin: (LoginResponse) -> Unit,
  onRegisterTapped: () -> Unit,
)

Also, update the following inside LoginScreen:

movieDiaryApi.loginUser(username, password) { loginResponse, error ->
  if (loginResponse == null) {
    screenScope.launch {
      scaffoldState.snackbarHostState.showSnackbar(error?.message ?: "")
    }
  } else {
    onLogin(loginResponse)
  }
}

The lambda parameter just changed from String to LoginResponse. To match this change, go to MainActivity.kt and change the following:

onLogin = { loginResponse ->
  App.saveUserToken(loginResponse.token)
  userLoggedIn = true
  currentScreen = Screens.MOVIES
}

Build and run the app. Everything should work the same as before.

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