Networking with Retrofit

Android Development โ€” Architecture & Data
Course 2 ยท Chapter 4 ยท Networking with Retrofit

๐ŸŒ Networking with Retrofit

Retrofit is the standard Android HTTP client โ€” it turns a REST API into a Kotlin interface, converting JSON into typed data classes automatically. This chapter covers defining an API interface, JSON conversion, and wrapping the result in the same sealed-class pattern already seen in Kotlin Intermediate Chapter 2's Flow example, now used for real network error handling.

๐Ÿ“ก Defining an API Interface

data class User( val id: Int, val name: String, val email: String ) interface ApiService { @GET("users") suspend fun getUsers(): List<User> @GET("users/{id}") suspend fun getUser(@Path("id") id: Int): User @POST("users") suspend fun createUser(@Body user: User): User }

There's no implementation here at all โ€” ApiService is just an interface with annotated method signatures; Retrofit generates the actual networking code behind it. suspend fun means each call is a genuine Kotlin Intermediate Chapter 1 suspend function โ€” no callbacks, no manual thread-switching, and the JSON response comes back already converted into the declared return type.

๐Ÿ”„ JSON Conversion โ€” Gson or Moshi

Retrofit itself doesn't parse JSON โ€” a converter library does, plugged in as a converter factory:

// Moshi (the more modern, Kotlin-friendly choice) data class User( val id: Int, val name: String, @Json(name = "email_address") val email: String // maps a different JSON key name to this property )

Gson vs Moshi

GsonMoshi
Reflection-basedYesOptional โ€” can use compile-time code generation instead
Kotlin null-safety awarenessLimited โ€” can silently produce nulls for non-null typesBetter โ€” respects Kotlin nullability more strictly
Renaming a JSON field@SerializedName("email_address")@Json(name = "email_address")

Either works and both are widely used in real projects; Moshi is generally the more actively recommended choice for new Kotlin code specifically because of its stricter null-safety behavior, which matters given how central null safety is to the rest of this curriculum (Kotlin Fundamentals Chapter 3).

๐Ÿ—๏ธ Building the Retrofit Instance

val retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(MoshiConverterFactory.create()) .build() val apiService: ApiService = retrofit.create(ApiService::class.java)

retrofit.create(ApiService::class.java) is Kotlin Intermediate Chapter 6's reflection in action, though wrapped away inside Retrofit's own library code โ€” it dynamically generates a real implementation of the ApiService interface at runtime, wiring each annotated method to an actual HTTP call. Like the database singleton pattern from last chapter, one shared Retrofit instance (and one shared ApiService) is normal for an entire app, not recreated per screen.

โš ๏ธ Error Handling โ€” Reusing a Familiar Sealed Class

Kotlin Intermediate Chapter 2 introduced exactly this shape for a different reason (handling a Flow's possible states) โ€” the same sealed class pattern is the standard way to represent "a network call that might succeed, fail, or still be in progress":

sealed class NetworkResult<out T> data class Success<T>(val data: T) : NetworkResult<T>() data class Error(val message: String) : NetworkResult<Nothing>() object Loading : NetworkResult<Nothing>() suspend fun fetchUsers(api: ApiService): NetworkResult<List<User>> { return try { NetworkResult.Success(api.getUsers()) } catch (e: IOException) { NetworkResult.Error("No internet connection") } catch (e: HttpException) { NetworkResult.Error("Server error: ${e.code()}") } }

NetworkResult<out T> uses Kotlin Intermediate Chapter 3's covariance โ€” out T is what allows Error and Loading to both implement NetworkResult<Nothing> while still being treated as a NetworkResult<List<User>> (or any other T) where needed, since they never actually need to hold a real T value. IOException generally means "never reached the server" (no connection, DNS failure); HttpException means "reached the server, got back an error status code" โ€” distinguishing them gives a more useful error message than one generic catch-all.

๐Ÿ“ฒ Loading States in the ViewModel

This connects directly to Chapter 2's single-UiState-per-screen pattern:

data class UsersUiState( val isLoading: Boolean = false, val users: List<User> = emptyList(), val errorMessage: String? = null ) class UsersViewModel(private val api: ApiService) : ViewModel() { private val _uiState = MutableStateFlow(UsersUiState()) val uiState: StateFlow<UsersUiState> = _uiState.asStateFlow() init { loadUsers() } fun loadUsers() { viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true) when (val result = fetchUsers(api)) { is NetworkResult.Success -> _uiState.value = _uiState.value.copy(isLoading = false, users = result.data) is NetworkResult.Error -> _uiState.value = _uiState.value.copy(isLoading = false, errorMessage = result.message) NetworkResult.Loading -> Unit // not produced by fetchUsers here, but the when must stay exhaustive } } } }

Every earlier architectural piece shows up together here: init { } (Kotlin Fundamentals Chapter 4) triggers the initial load, viewModelScope.launch (this chapter's Retrofit calls are suspend functions, needing a coroutine), and the exhaustive when (Kotlin Fundamentals Chapter 5's sealed class matching) ensures every possible NetworkResult case updates the UI state correctly.

Retrofit vs JavaScript fetch/axios

JavaScript (fetch/axios)Retrofit
Defining an endpointA URL string passed to fetch()/axios.get()An annotated interface method
JSON parsingresponse.json() (manual, untyped by default)Automatic, into a typed data class (Gson/Moshi)
Async modelPromises / async-awaitKotlin suspend functions
Error handlingtry/catch around fetch, checking response.oktry/catch around the suspend call, per-exception-type

๐Ÿ’ป Coding Challenges

Challenge 1: An API Interface

Define a data class Post(val id: Int, val title: String, val body: String) and an interface PostApiService with a suspend getPosts(): List<Post> GET endpoint and a suspend getPost(id: Int): Post GET endpoint using @Path. Build a Retrofit instance and PostApiService for https://jsonplaceholder.typicode.com/ using a converter factory of your choice.

Goal: Practice defining a Retrofit API interface and building the client instance.

โ†’ Solution

Challenge 2: NetworkResult Error Handling

Write a suspend fun fetchPosts(api: PostApiService): NetworkResult<List<Post>> using the chapter's NetworkResult sealed class, catching both IOException and HttpException with distinct error messages.

Goal: Practice the try/catch-into-sealed-class error wrapping pattern.

โ†’ Solution

Challenge 3: A Full ViewModel with Loading State

Write a PostsUiState data class (isLoading, posts, errorMessage) and a PostsViewModel that loads posts on init using Challenge 2's fetchPosts(), updating uiState correctly through loading, success, and error paths using an exhaustive when.

Goal: Practice the complete Retrofit-to-UiState pipeline end to end.

โ†’ Solution

๐Ÿ’ก Notice How Little Was Genuinely New

Suspend functions, sealed classes, covariance, data class copy(), reflection, property delegation โ€” every one of them from earlier chapters, recombined for a new purpose. Networking in a real Android app isn't a separate skill so much as it's these same tools applied to yet another data source, joining local storage (Room, last chapter) as another Flow/StateFlow feeding the same UI pattern.

๐ŸŽฏ What's Next

Next chapter: Dependency Injection with Hilt โ€” why DI matters, Hilt setup, modules, and injecting into a ViewModel.