Start by adding suspend and changing the return types of all methods in MovieDiaryApiService.
kt like this:
@POST("user/register")
suspend fun registerUser(@Body body: RegisterBody): Response<Unit>
@POST("user/login")
suspend fun loginUser(@Body registerBody: RegisterBody): LoginResponse
@GET("sampleMovies")
suspend fun getMovies(): List<MovieReview>
@GET("user")
suspend fun getProfile(): User
@POST("movies")
suspend fun postReview(@Body movieReview: MovieReview): MovieReview
registerUser() has the return type of Response<Unit> because you need the response metadata to
determine if the response was successful. All other methods return either a custom type or a
list of types.
With the interface methods ready, go to MovieDiaryApi.kt and start by deleting the existing
registerUser() and adding the following code:
suspend fun registerUser(
username: String,
email: String,
password: String,
): Result<String> {
}
Note three things here:
- This is now a suspending function because it’ll call a suspending function from the API interface.
- You no longer need to pass in a callback to handle the result.
-
You’ll return an instance of the Kotlin
Resultclass to the caller.
Now, add the following code to the body:
suspend fun registerUser(
username: String,
email: String,
password: String,
): Result<String> = try {
val body = RegisterBody(username, email, password)
val result = apiService.registerUser(body)
if (result.isSuccessful) {
Result.success(result.message())
} else {
Result.failure(Throwable(result.errorBody()?.string()))
}
} catch (error: Throwable) {
Result.failure(error)
}
You now use try/catch for simple and intuitive error handling. To check whether the response
is successful, you use the same result.isSuccessful call as before. When
processing the response, instead of invoking the callback, you now simply return success with
some data if the response is OK or failure if the server responds with an error. This code
already looks much nicer and simpler compared with the standard Retrofit approach with callbacks.
But you can make it even more concise.
Replace loginUser() with the following code:
suspend fun loginUser(
username: String,
password: String,
): Result<LoginResponse> {
return runCatching { apiService.loginUser(RegisterBody(username = username,password = password))}
}
Instead of using try/catch, you use runCatching here. If you’re not familiar with this
construct, it’s a wrapper around try/catch that runs the given block of code, catching any errors
thrown by the block code execution. In case of a successful invocation, it returns the value
encapsulated as Result.success(). If an error occurs, it’s returned as Result.failure.
Refactor getMovies() in the same way. Replace the existing method with this code:
suspend fun getMovies(): Result<List<MovieReview>> = runCatching { apiService.getMovies() }
This method is a great example of how simple and readable your networking code can be when using Retrofit with Kotlin coroutines.
With the networking code refactored, you must make some changes to the calling code in the UI layer as well. Start by going to RegisterScreen.kt and replacing the existing API call with the following:
movieDiaryApi.registerUser(username, email, password)
.onSuccess {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onUserRegistered()
}.onFailure { scaffoldState.snackbarHostState.showSnackbar(it.message ?: "") }
Having Result as the return type allows for a readable and clean code, using
onSuccess and onFailure to handle success and error cases, respectively. If the
registration succeeds, you invoke onUserRegistered(). In case of a failure, you show a
Snackbar with the error message.
LoginScreen.kt also requires a small refactor. Open the file and add the following code:
movieDiaryApi.loginUser(username, password)
.onSuccess(onLogin)
.onFailure { scaffoldState.snackbarHostState.showSnackbar(it.message ?: "") }
As with registerUser(), the code is simple. If the API request succeeds, invoke
onLogin; otherwise, show a Snackbar with an error message.
You still have MoviesScreen to fix. Head over to MoviesScreen.kt and add this piece of code:
LaunchedEffect(Unit) {
movieDiaryApi.getMovies()
.onSuccess { movieReviewList = it }
.onFailure { scaffoldState.snackbarHostState.showSnackbar(it.message ?: "") }
}
You wrap the API call in LaunchedEffect(Unit) because you don’t want to trigger a new API call
every time a recomposition happens. LaunchedEffect launches a new coroutine when it enters the
composition, and the coroutine gets canceled when it leaves the composition. You must pass in
a key to LaunchedEffect. Whenever LaunchedEffect recomposes with a different key, the
coroutine gets canceled and re-launched. Because you passed Unit as a key, it’ll never
relaunch the coroutine.
Build and run the app now. If you want, you can try to register a new user or log in. Everything should work as before.