Techbase Academy
Android · Lesson 29

Get Data from the Internet: HTTP, REST and Retrofit

11 phasesPosts Reader with RetryAndroid Studio
Phase 1 of 11
Lesson Introduction

Almost every app you use gets its data from somewhere else: your bank balance, the football scores, the weather, the posts in your feed. The data lives on a server, and your app asks for it over the internet. In this lesson you learn how that conversation works (HTTP and REST), what the data looks like (JSON), and how to fetch it in a few lines with Retrofit and turn it into Kotlin objects with kotlinx.serialization.

This lesson covers the two remaining items of Pathway 1, Unit 5 of the official Android Basics with Compose course: Introduction to HTTP/REST and Get data from the internet (the codelab in which you build the Mars Photos app). We build the same architecture with our own data source, a free test service that serves fake blog posts, because public demo servers come and go. The pattern is identical for any real API.

By the end you will be able to:

  • Describe a request and response: URL, method, headers, status code, body.
  • Read and write JSON.
  • Add the INTERNET permission and explain why it is needed.
  • Define a Retrofit service interface and build a Retrofit object.
  • Parse JSON into @Serializable data classes.
  • Model Loading, Success and Error with a sealed interface and show each in Compose.
  • Handle failures (no network, server errors, bad JSON) without crashing.

Big idea: an HTTP call is a function call that might take a second, might fail, and returns text. Your job is to wrap it so that the UI always has something sensible to show.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 11
Prerequisite Concepts
  • Coroutines and suspend functions (lesson 28). Retrofit's suspend functions are main-safe.
  • ViewModel, StateFlow and UI state (lessons 24 and 25).
  • Sealed types and when (lesson 10). A sealed interface limits the possible subtypes, so when can be exhaustive.
  • Data classes (lesson 11).
  • Gradle version catalog (lessons 24 and 28).
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 11
Conceptual Understanding: HTTP and REST

The client and the server

When your app (the client) wants data it sends an HTTP request to a server. The server replies with an HTTP response. Think of ordering at a counter: you say what you want, the cook says "here it is" or "we have run out".

text
 App (client)                                     Server
     |   GET /posts/1  HTTP/1.1                      |
     |   Host: jsonplaceholder.typicode.com          |
     |   Accept: application/json                    |
     | --------------------------------------------> |
     |                                               |
     |   HTTP/1.1 200 OK                             |
     |   Content-Type: application/json              |
     |                                               |
     |   {"userId":1,"id":1,"title":"...","body":"..."}
     | <-------------------------------------------- |

Parts of a request

Part Example Meaning
URL https://example.com/api/posts?userId=1 Where to go. Scheme, host, path and query (?userId=1).
Method GET, POST, PUT, DELETE What to do.
Headers Accept: application/json, Authorization: Bearer ... Extra information.
Body {"title":"Hello"} Data you send (POST, PUT).

The methods map to everyday verbs:

Method Meaning Changes data?
GET Read something No
POST Create something Yes
PUT / PATCH Replace or update something Yes
DELETE Remove something Yes

Status codes

The response begins with a three-digit status code:

Range Meaning Examples
2xx Success 200 OK, 201 Created
3xx Redirect 301 Moved Permanently
4xx Your mistake 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
5xx Server problem 500 Internal Server Error, 503 Service Unavailable

REST

REST is a style for designing web services. Data is organised as resources with URLs, and you use HTTP methods on them:

text
GET    /posts        -> list of posts
GET    /posts/7      -> post number 7
POST   /posts        -> create a post
DELETE /posts/7      -> delete post 7

A service that follows this style is called a RESTful web service, and its address list is its API (application programming interface). Most REST services return JSON.

JSON

JSON is plain text for structured data, with two building blocks: objects ({ "key": value }) and arrays ([ ... ]). Values can be strings, numbers, true/false, null, objects or arrays.

json
[
 { "userId": 1, "id": 1, "title": "First post", "body": "Hello world" },
 { "userId": 1, "id": 2, "title": "Second post", "body": "More text" }
]

The outer [ ] is a list; each { } is one object whose keys correspond to Kotlin property names. Parsing JSON means converting that text to Kotlin objects, and serialization is the reverse.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: HTTP and REST. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 11
Step-by-Step Build 1: Project Setup

Create a new Empty Activity project called Posts (package com.example.posts).

The INTERNET permission

Apps cannot use the network unless the manifest asks for it. Open app/src/main/AndroidManifest.xml and add the line above the <application> tag:

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 <uses-permission android:name="android.permission.INTERNET" />
 <application
 ...

INTERNET is a normal permission: the system grants it automatically at install time and the user is not asked. Forgetting it is the most common reason a first network call fails (with an UnknownHostException or a permission error in Logcat).

Dependencies

Add three libraries: Retrofit, the converter that links Retrofit to kotlinx.serialization, and kotlinx.serialization itself (plus its Gradle plugin). Look up current stable versions at the Retrofit releases page (https://github.com/square/retrofit/releases) and the kotlinx.serialization releases page (https://github.com/Kotlin/kotlinx.serialization/releases). Retrofit includes an official converter, com.squareup.retrofit2:converter-kotlinx-serialization, published from 2.10.0 onwards.

libs.versions.toml
[versions]
retrofit = "<latest stable>"
kotlinxSerializationJson = "<latest stable>"
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
build.gradle.kts
plugins {
 alias(libs.plugins.kotlin.serialization)
}
dependencies {
 implementation(libs.retrofit)
 implementation(libs.retrofit.converter.kotlinx.serialization)
 implementation(libs.kotlinx.serialization.json)
}

(Also add alias(libs.plugins.kotlin.serialization) apply false to the root build.gradle.kts plugins block, as in lesson 26.) Click Sync Now.

Why these libraries? Retrofit turns an annotated Kotlin interface into working HTTP code. It runs on top of OkHttp, which does the actual networking and comes with it automatically. kotlinx.serialization is Kotlin's official JSON tool and the compiler plugin generates the parsing code at build time (no reflection, so it is fast and works with code shrinking).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Project Setup. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 11
Step-by-Step Build 2: Model, Service and Retrofit Object

The data class

The test service returns posts with four fields. Create Post.kt:

Post.kt
package com.example.posts.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Post(
 val id: Int,
 val title: String,
 val body: String,
 @SerialName("userId") val authorId: Int
)
  • @Serializable tells the compiler plugin to generate the converter code.
  • Property names must match the JSON keys, unless you rename with @SerialName("jsonKey"), as done for authorId.
  • Types must match the JSON. A number cannot be read as a String by default.

The service interface

Describe the API as an interface. Retrofit generates the implementation.

PostsApiService.kt
package com.example.posts.network
import com.example.posts.model.Post
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface PostsApiService {
 @GET("posts")
 suspend fun getPosts(): List<Post>
 @GET("posts/{id}")
 suspend fun getPost(@Path("id") id: Int): Post
 @GET("posts")
 suspend fun getPostsByAuthor(@Query("userId") authorId: Int): List<Post>
}

Each function is one endpoint:

  • @GET("posts") means GET <baseUrl>/posts.
  • {id} in the path is replaced by the @Path("id") argument.
  • @Query("userId") adds ?userId=3 to the URL.
  • suspend makes the function main-safe and lets you call it from a coroutine. Retrofit runs the request on a background thread for you, so you do not need withContext(Dispatchers.IO).
  • The return type is the parsed result. If the server returns 404 or 500, Retrofit's suspend functions throw HttpException. If there is no network, they throw IOException.

Building Retrofit

PostsApi.kt
package com.example.posts.network
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
private val json = Json {
 ignoreUnknownKeys = true // do not crash if the server adds new fields
}
private val retrofit: Retrofit = Retrofit.Builder()
 .baseUrl(BASE_URL)
 .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
 .build()
object PostsApi {
 val service: PostsApiService by lazy { retrofit.create(PostsApiService::class.java) }
}

Notes:

  • The base URL must end with a /. Retrofit throws an exception at startup if it does not.
  • jsonplaceholder.typicode.com is a free, public test service built for exactly this kind of practice, with a /posts endpoint returning 100 fake posts. It is a third-party service and may change or go offline, so if it does not respond, any JSON-over-HTTPS service you control or trust works the same way; only the base URL and the data class change.
  • ignoreUnknownKeys = true makes the parser ignore JSON keys that your class does not declare. Without it, parsing fails the day the server adds a field. The default is strict, so setting this is a safe habit.
  • by lazy builds the service the first time it is used and then reuses it. Building a Retrofit object is expensive, so create one and share it. In lesson 30 you will move this into a dependency container.
  • asConverterFactory is an extension function on Json; import it from retrofit2.converter.kotlinx.serialization. The toMediaType extension comes from OkHttp, which Retrofit brings in.

If you want a quick sanity check before writing any UI, open the URL https://jsonplaceholder.typicode.com/posts in a browser on your computer and look at the JSON.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Model, Service and Retrofit Object. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 11
Step-by-Step Build 3: UI State and ViewModel

A network screen is never just "the data". It is one of three situations: loading, success or error. Model them with a sealed interface:

PostsUiState.kt
package com.example.posts.ui
import com.example.posts.model.Post
sealed interface PostsUiState {
 data class Success(val posts: List<Post>) : PostsUiState
 data class Error(val message: String) : PostsUiState
 data object Loading : PostsUiState
}

data object (Kotlin 1.9 and later) is a singleton with a readable toString. Because the interface is sealed, when (state) must cover all three, and the compiler tells you if you forget one.

PostsViewModel.kt
package com.example.posts.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.posts.network.PostsApi
import java.io.IOException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.SerializationException
import retrofit2.HttpException
class PostsViewModel : ViewModel() {
 private val _uiState = MutableStateFlow<PostsUiState>(PostsUiState.Loading)
 val uiState: StateFlow<PostsUiState> = _uiState.asStateFlow()
 init {
 loadPosts()
 }
 fun loadPosts() {
 viewModelScope.launch {
 _uiState.value = PostsUiState.Loading
 _uiState.value = try {
 PostsUiState.Success(PostsApi.service.getPosts())
 } catch (e: IOException) {
 PostsUiState.Error("No connection. Check your data or Wi-Fi and try again.")
 } catch (e: HttpException) {
 PostsUiState.Error("The server returned an error (${e.code()}).")
 } catch (e: SerializationException) {
 PostsUiState.Error("The server sent data we could not read.")
 }
 }
 }
}

The three catch blocks match the three ways a call can fail:

Exception Meaning Example
IOException The request never completed Airplane mode, timeout, no signal
HttpException The server answered with an error code 404, 500
SerializationException The response arrived but was not what we expected Wrong JSON shape

CancellationException is deliberately not caught, so cancellation still works (lesson 28). Catching broad Exception is tempting but would swallow it.

Showing the user a clear, human message for each case matters on real phones in real places: mobile data is patchy, so error states are not a rare edge case but part of normal life, particularly when users switch between networks.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: UI State and ViewModel. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 11
Step-by-Step Build 4: The Screen
PostsScreen.kt
package com.example.posts.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.posts.model.Post
@Composable
fun PostsApp(modifier: Modifier = Modifier, viewModel: PostsViewModel = viewModel()) {
 val state by viewModel.uiState.collectAsStateWithLifecycle()
 PostsScreen(state = state, onRetry = viewModel::loadPosts, modifier = modifier)
}
@Composable
fun PostsScreen(state: PostsUiState, onRetry: () -> Unit, modifier: Modifier = Modifier) {
 when (state) {
 is PostsUiState.Loading -> Column(
 modifier = modifier.fillMaxSize(),
 verticalArrangement = Arrangement.Center,
 horizontalAlignment = Alignment.CenterHorizontally
 ) { CircularProgressIndicator() }
 is PostsUiState.Error -> Column(
 modifier = modifier.fillMaxSize().padding(24.dp),
 verticalArrangement = Arrangement.Center,
 horizontalAlignment = Alignment.CenterHorizontally
 ) {
 Text(state.message)
 Button(onClick = onRetry, modifier = Modifier.padding(top = 16.dp)) { Text("Try again") }
 }
 is PostsUiState.Success -> LazyColumn(
 modifier = modifier,
 contentPadding = PaddingValues(8.dp),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(state.posts, key = { it.id }) { post -> PostCard(post) }
 }
 }
}
@Composable
fun PostCard(post: Post, modifier: Modifier = Modifier) {
 Card(modifier = modifier) {
 Column(Modifier.padding(16.dp)) {
 Text(post.title, style = MaterialTheme.typography.titleMedium)
 Text(post.body, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp))
 }
 }
}

(Add import androidx.compose.foundation.layout.PaddingValues.) Call PostsApp(Modifier.padding(innerPadding)) from your Scaffold in MainActivity.

Run it. You should see a spinner for a moment and then a scrolling list of posts. Now test the failure paths, which is where many apps are fragile:

  1. Airplane mode on, then relaunch or tap Try again: you should see the "No connection" message, not a crash.
  2. Change the base path in the interface to @GET("postz") and run: you should see the error message with code 404.
  3. Remove the INTERNET permission and observe the error; restore it.

Open Logcat while experimenting and read the exception type for each case. In the Android emulator you can also switch the network off in the emulator's Extended controls panel (the three-dot button) under Cellular or Wi-Fi.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: The Screen. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 11
Seeing What Goes Over the Wire

When a call misbehaves, you want to see the request and response. OkHttp has a logging interceptor for that (com.squareup.okhttp3:logging-interceptor). Add it to the Retrofit client for debug builds only:

kotlin
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
val client = OkHttpClient.Builder()
 .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC })
 .build()
Retrofit.Builder()
 .baseUrl(BASE_URL)
 .client(client)
 .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
 .build()

Level.BASIC logs the method, URL and status. Level.BODY logs everything, including headers and body, which can contain passwords and tokens, so never leave it on in a release build. This is a security point, not just tidiness.

Security basics for networking

  • Always use https://. Android blocks plain http:// traffic by default for apps that target recent versions, for good reason: anyone on the same Wi-Fi can read it.
  • Never hard-code secret API keys in your app. Anything in an APK can be extracted. Keys that must stay secret belong on your server, which calls the third-party service for you.
  • Do not log personal data.
✏️ Your Task
Practise what you just learned about Seeing What Goes Over the Wire. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 11
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Fetch one post

Add a detail screen to the app. Tapping a card should call getPost(id) and show the full post. Model the detail state with its own sealed interface. For this exercise, keep it simple: navigate by changing a selectedId in the ViewModel rather than by using the Navigation library.

kotlin
sealed interface DetailUiState {
 data object Loading : DetailUiState
 data class Success(val post: Post) : DetailUiState
 data class Error(val message: String) : DetailUiState
}
// In PostsViewModel
private val _detail = MutableStateFlow<DetailUiState?>(null) // null = list is showing
val detail: StateFlow<DetailUiState?> = _detail.asStateFlow()
fun openPost(id: Int) {
 viewModelScope.launch {
 _detail.value = DetailUiState.Loading
 _detail.value = try {
 DetailUiState.Success(PostsApi.service.getPost(id))
 } catch (e: IOException) {
 DetailUiState.Error("No connection.")
 } catch (e: HttpException) {
 DetailUiState.Error("Server error ${e.code()}.")
 }
 }
}
fun closePost() { _detail.value = null }

In the UI, show the detail when detail is not null and add a BackHandler(onBack = viewModel::closePost). In a real app you would use Navigation (lesson 26) with the ID in a type-safe route and a detail ViewModel loading by ID.

🎯 Your Challenge

Exercise 2: Filter by author

Add a row of three buttons (Author 1, Author 2, Author 3) above the list that call getPostsByAuthor(n). Show which author is selected. Make sure a slow earlier request cannot overwrite the result of a later one.

kotlin
private var loadJob: Job? = null
fun loadByAuthor(authorId: Int) {
 loadJob?.cancel() // stop the earlier request
 loadJob = viewModelScope.launch {
 _uiState.value = PostsUiState.Loading
 _uiState.value = try {
 PostsUiState.Success(PostsApi.service.getPostsByAuthor(authorId))
 } catch (e: IOException) {
 PostsUiState.Error("No connection.")
 } catch (e: HttpException) {
 PostsUiState.Error("Server error ${e.code()}.")
 }
 }
}

Why cancel? Tap Author 1, then quickly Author 2. Without cancelling, the slower first response may arrive after the second and overwrite it with the wrong list (a race condition). Cancelling the earlier Job guarantees only the latest request can publish a result. Store the selected author in the UI state so the buttons can show which one is active.

✏️ Your Task

Open https://jsonplaceholder.typicode.com/posts/1 in a browser (or any JSON API you like) and write a matching @Serializable data class by hand. Then deliberately make one property the wrong type and read the exception in Logcat.

Phase 10 of 11
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Missing INTERNET permission. Symptom: the call fails immediately with a host or permission error. The fix: add <uses-permission android:name="android.permission.INTERNET" /> above <application>.

Mistake #2: Base URL without a trailing slash. Symptom: a crash at startup saying the base URL must end in /. The fix: "https://host/".

Mistake #3: Forgetting the serialization plugin. Symptom: Serializer for class 'Post' is not found. The fix: apply the org.jetbrains.kotlin.plugin.serialization plugin and annotate the class with @Serializable.

Mistake #4: JSON key and property mismatch. Symptom: Field 'x' is required but it was missing. The fix: match names exactly or use @SerialName; give optional fields a default value (val body: String = "") or make them nullable.

Mistake #5: Not handling errors. What happens: the app crashes whenever the user is offline. The fix: catch IOException, HttpException and SerializationException, and show a retry button.

Mistake #6: Creating Retrofit repeatedly. What happens: slow and wasteful. The fix: build one instance and share it.

Mistake #7: Plain http:// URLs. What happens: cleartext traffic is blocked by default and the call fails. The fix: use https.

Mistake #8: Logging full bodies in release builds. What happens: secrets in logs. The fix: log only in debug builds and at a low level.

Mistake #9: Blocking the main thread with a non-Retrofit call. What happens: NetworkOnMainThreadException. The fix: keep network calls in suspend functions that Retrofit executes off the main thread.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 11
Knowledge Check

Question 1. What are the main parts of an HTTP request?

A URL, a method (GET, POST and so on), headers, and optionally a body.

Question 2. What do the status code ranges 2xx, 4xx and 5xx mean?

2xx means success, 4xx means a client error (the request was wrong, for example 404 Not Found), and 5xx means a server error.

Question 3. What is REST?

An architectural style for web services where data is exposed as resources with URLs and manipulated using standard HTTP methods; responses are usually JSON.

Question 4. Why is a sealed interface a good fit for UI state in a network screen?

It limits the state to a fixed set (Loading, Success, Error), so when can be exhaustive and the compiler warns about unhandled cases; impossible combinations cannot be represented.

Question 5. Which exceptions should a network call handle, and what does each mean?

IOException for connectivity problems, HttpException for error status codes from the server, and SerializationException for unparseable or mismatched data.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Posts Reader with Retry

Project goal: Finish the Posts app as a small, solid product.

Requirements:

  1. Show the list of posts in cards with title and body (body limited to three lines with an ellipsis).
  2. Pull the data in init and let the user retry from the error state.
  3. Add a search box that filters the loaded posts by title (client side; the filter text is part of the UI state).
  4. Show a different message for no connection and for a server error.
  5. Debug-only logging interceptor.
  6. A ViewModel unit test using a fake service (an object implementing PostsApiService) that returns a short list, throws an IOException, or throws an HttpException. Check each resulting state. (The ViewModel needs to accept the service as a constructor parameter, which lesson 30 explains in detail.)

Stretch goal: cache the last successful list in memory so that an error on refresh keeps the old data visible with a banner.

PostsViewModel.kt
package com.example.posts.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.posts.model.Post
import com.example.posts.network.PostsApi
import com.example.posts.network.PostsApiService
import java.io.IOException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.HttpException
data class PostsScreenState(
 val loading: Boolean = true,
 val posts: List<Post> = emptyList(),
 val error: String? = null,
 val query: String = ""
) {
 val visiblePosts: List<Post>
 get() = if (query.isBlank()) posts
 else posts.filter { it.title.contains(query.trim(), ignoreCase = true) }
}
class PostsViewModel(
 private val service: PostsApiService = PostsApi.service
) : ViewModel() {
 private val _state = MutableStateFlow(PostsScreenState())
 val state: StateFlow<PostsScreenState> = _state.asStateFlow()
 init { load() }
 fun load() {
 viewModelScope.launch {
 _state.update { it.copy(loading = true, error = null) }
 try {
 val posts = service.getPosts()
 _state.update { it.copy(loading = false, posts = posts) }
 } catch (e: IOException) {
 _state.update { it.copy(loading = false, error = "No connection. Try again.") }
 } catch (e: HttpException) {
 _state.update { it.copy(loading = false, error = "Server error (${e.code()}).") }
 }
 }
 }
 fun onQueryChange(text: String) = _state.update { it.copy(query = text) }
}
PostsViewModelTest.kt
package com.example.posts
import com.example.posts.model.Post
import com.example.posts.network.PostsApiService
import com.example.posts.ui.PostsViewModel
import java.io.IOException
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Rule
import org.junit.Test
private class FakeService(private val action: () -> List<Post>) : PostsApiService {
 override suspend fun getPosts(): List<Post> = action()
 override suspend fun getPost(id: Int): Post = error("not used")
 override suspend fun getPostsByAuthor(authorId: Int): List<Post> = error("not used")
}
class PostsViewModelTest {
 @get:Rule val mainRule = MainDispatcherRule() // from lesson 25
 private val sample = listOf(
 Post(1, "Kotlin tips", "body", 1),
 Post(2, "Compose basics", "body", 1)
 )
 @Test
 fun success_showsPosts() = runTest {
 val vm = PostsViewModel(FakeService { sample })
 advanceUntilIdle()
 assertEquals(2, vm.state.value.posts.size)
 }
 @Test
 fun ioException_showsNoConnection() = runTest {
 val vm = PostsViewModel(FakeService { throw IOException("offline") })
 advanceUntilIdle()
 assertNotNull(vm.state.value.error)
 }
 @Test
 fun search_filtersByTitle() = runTest {
 val vm = PostsViewModel(FakeService { sample })
 advanceUntilIdle()
 vm.onQueryChange("compose")
 assertEquals(listOf(2), vm.state.value.visiblePosts.map { it.id })
 }
}

Notes: the ViewModel takes the service as a constructor parameter with a default, which is the first step toward dependency injection (lesson 30). The viewModel() function can still create it because every parameter has a default value. Building an HttpException in a test needs a retrofit2.Response, which is awkward, so the sample keeps to IOException; you can add the HTTP case by constructing HttpException(Response.error<Any>(500, "".toResponseBody())). The screen is the when/LazyColumn pattern from Build 4, with an OutlinedTextField bound to state.query.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 29 project”.

3

On github.com click New repository, name it android-lesson-29-get-data-from-the-internet-http-rest-and-r, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 29 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: HTTP and REST
  • Step-by-Step Build 1: Project Setup
  • Step-by-Step Build 2: Model, Service and Retrofit Object
  • Step-by-Step Build 3: UI State and ViewModel
  • Step-by-Step Build 4: The Screen
  • Seeing What Goes Over the Wire
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close