Chapter 9: Networking and APIs
Networking and APIs! This is a massive milestone — your app is about to stop being offline-only and start talking to the real world. Fetching data from servers (like weather, news, user profiles, or even memes) is what makes most modern Android apps useful.
In January 2026, for Android + Kotlin apps:
- Retrofit (latest stable: 3.0.0 from May 2025, fully Kotlin-rewritten) is still the most popular choice (~70-75% of Android devs per recent surveys). It’s battle-tested, has huge ecosystem (converters for Moshi/Gson/Kotlinx Serialization, interceptors, etc.), and integrates beautifully with Jetpack Compose.
- Ktor Client (latest: 3.4.0 from January 23, 2026) is gaining fast traction, especially in Kotlin-first / Kotlin Multiplatform (KMP) projects. It’s fully coroutine-native, lightweight, more flexible for custom HTTP logic, and great if you’re planning iOS/desktop sharing later. But it has a smaller Android-specific community compared to Retrofit.
Recommendation for you right now (as a learner building Android apps):
- Use Retrofit + Kotlinx Serialization (or Moshi) for this chapter and most projects — it’s easier to learn, has better docs/tutorials, and you’ll see it in 90% of job codebases.
- Switch to Ktor later if you go multiplatform or want more control.
We’ll use Retrofit here, but I’ll mention Ktor equivalents briefly.
Key ingredients:
- HTTP client (Retrofit)
- JSON parsing (Kotlinx Serialization – modern, Kotlin-idiomatic)
- Coroutines (for async, non-blocking network calls)
- Display in Compose
1. Retrofit Basics (Type-Safe HTTP Client)
Retrofit turns your REST API into a clean Kotlin interface.
Add dependencies (app/build.gradle.kts – use latest stable):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
val retrofit_version = "3.0.0" val serialization_version = "1.7.3" // Kotlinx Serialization implementation("com.squareup.retrofit2:retrofit:$retrofit_version") implementation("com.squareup.retrofit2:converter-kotlinx-serialization:$retrofit_version") // Converter implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") // If not already |
2. Define API Interface
Use annotations – super clean!
Example: We’ll fetch posts from JSONPlaceholder (free fake REST API – perfect for tutorials, no key needed).
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// ApiService.kt import retrofit2.http.GET import kotlinx.serialization.Serializable @Serializable data class Post( val id: Int, val userId: Int, val title: String, val body: String ) interface ApiService { @GET("posts") suspend fun getPosts(): List<Post> // suspend = coroutine-friendly } |
- @GET(“posts”) → Endpoint relative to base URL.
- suspend → Lets us call it inside coroutine scope without blocking.
3. Create Retrofit Instance
Usually in a singleton/object (or via Hilt/Dagger later).
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// RetrofitInstance.kt import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType object RetrofitInstance { private val json = Json { ignoreUnknownKeys = true } // Forgiving parser val api: ApiService by lazy { Retrofit.Builder() .baseUrl("https://jsonplaceholder.typicode.com/") .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() .create(ApiService::class.java) } } |
- ignoreUnknownKeys → Safe if API adds extra fields.
- Lazy → Created only when first used.
4. Coroutines for Async (No More Callback Hell)
Network calls must be off main thread. Use viewModelScope + suspend.
In ViewModel:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class PostsViewModel : ViewModel() { private val _posts = mutableStateListOf<Post>() val posts: List<Post> get() = _posts private val _isLoading = mutableStateOf(false) val isLoading: State<Boolean> = _isLoading private val _error = mutableStateOf<String?>(null) val error: State<String?> = _error init { fetchPosts() } fun fetchPosts() { viewModelScope.launch { _isLoading.value = true _error.value = null try { val response = RetrofitInstance.api.getPosts() _posts.clear() _posts.addAll(response) } catch (e: Exception) { _error.value = "Failed to load: ${e.message}" } finally { _isLoading.value = false } } } } |
- viewModelScope.launch → Coroutine on IO thread by default (safe for network).
- Try-catch → Handle errors (no internet, 404, etc.).
- State updates → Trigger Compose recomposition.
5. Display in Compose (LazyColumn + Loading/Error States)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@Composable fun PostsScreen(viewModel: PostsViewModel = viewModel()) { val posts = viewModel.posts val isLoading by viewModel.isLoading val error by viewModel.error Column(modifier = Modifier.fillMaxSize()) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) } else if (error != null) { Text( text = error ?: "Unknown error", color = Color.Red, modifier = Modifier.padding(16.dp) ) Button(onClick = { viewModel.fetchPosts() }) { Text("Retry") } } else if (posts.isEmpty()) { Text("No posts found", modifier = Modifier.padding(16.dp)) } else { LazyColumn { items(posts, key = { it.id }) { post -> Card( modifier = Modifier .fillMaxWidth() .padding(8.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Text(post.title, style = MaterialTheme.typography.titleLarge) Spacer(Modifier.height(8.dp)) Text(post.body, style = MaterialTheme.typography.bodyMedium) } } } } } } } |
6. Hands-on: Fetch Real Data from Public API
Project: Posts Feed App
- Use the code above.
- Add to MainActivity setContent:
|
0 1 2 3 4 5 6 |
PostsScreen() |
- Run → See list of 100 fake posts load!
- Title + body in cards.
- Loading spinner first.
- Retry if you turn off internet.
Bonus Challenges:
- Add pull-to-refresh (use SwipeRefresh from accompanist or material3).
- Show user name (fetch /users/{userId} – make another suspend fun).
- Use Ktor instead (for comparison):
Ktor version (add implementation(“io.ktor:ktor-client-android:3.4.0”) + ktor-client-content-negotiation + ktor-serialization-kotlinx-json):
|
0 1 2 3 4 5 6 7 8 9 10 |
val client = HttpClient(Android) { install(ContentNegotiation) { json() } } suspend fun getPosts(): List<Post> = client.get("https://jsonplaceholder.typicode.com/posts").body() |
But stick with Retrofit for now – it’s what most jobs expect.
Common Pitfalls & Tips (2026 edition):
- Add <uses-permission android:name=”android.permission.INTERNET” /> in Manifest!
- Handle no-internet: Use ConnectivityManager or just catch IOException.
- Add logging: Add HttpLoggingInterceptor (OkHttp) for debug.
- Caching: Add Room + offline support later.
- Rate limits: JSONPlaceholder is unlimited, but real APIs need API keys (e.g., OpenWeather).
You’ve connected your app to the internet! Questions? Want to fetch real weather API? Add images from Unsplash? Handle auth tokens? Pagination with Paging 3? Tell me — next chapter: Architecture patterns (MVVM, Hilt). You’re building full-featured apps now — incredible progress! 🚀🌐
