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
Retrofitobject. - Parse JSON into
@Serializabledata 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.
- Coroutines and
suspendfunctions (lesson 28). Retrofit's suspend functions are main-safe. - ViewModel,
StateFlowand UI state (lessons 24 and 25). - Sealed types and
when(lesson 10). A sealed interface limits the possible subtypes, sowhencan be exhaustive. - Data classes (lesson 11).
- Gradle version catalog (lessons 24 and 28).
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".
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:
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.
[
{ "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.
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:
<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.
[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" }
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).
The data class
The test service returns posts with four fields. Create 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
)
@Serializabletells the compiler plugin to generate the converter code.- Property names must match the JSON keys, unless you rename with
@SerialName("jsonKey"), as done forauthorId. - Types must match the JSON. A number cannot be read as a
Stringby default.
The service interface
Describe the API as an interface. Retrofit generates the implementation.
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")meansGET <baseUrl>/posts.{id}in the path is replaced by the@Path("id")argument.@Query("userId")adds?userId=3to the URL.suspendmakes 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 needwithContext(Dispatchers.IO).- The return type is the parsed result. If the server returns
404or500, Retrofit's suspend functions throwHttpException. If there is no network, they throwIOException.
Building Retrofit
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.comis a free, public test service built for exactly this kind of practice, with a/postsendpoint 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 = truemakes 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 lazybuilds 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.asConverterFactoryis an extension function onJson; import it fromretrofit2.converter.kotlinx.serialization. ThetoMediaTypeextension 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.
A network screen is never just "the data". It is one of three situations: loading, success or error. Model them with a sealed interface:
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.
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.
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:
- Airplane mode on, then relaunch or tap Try again: you should see the "No connection" message, not a crash.
- Change the base path in the interface to
@GET("postz")and run: you should see the error message with code 404. - 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.
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:
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 plainhttp://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.
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.
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.
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.
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.
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.
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.
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
- Android Basics with Compose, Unit 5, Pathway 1: Get data from the internet: includes Introduction to HTTP/REST and the Mars Photos codelab.
- Android Basics with Compose course page
- Kotlin documentation: serialization
- Retrofit releases
Project goal: Finish the Posts app as a small, solid product.
Requirements:
- Show the list of posts in cards with title and body (body limited to three lines with an ellipsis).
- Pull the data in
initand let the user retry from the error state. - Add a search box that filters the loaded posts by title (client side; the filter text is part of the UI state).
- Show a different message for no connection and for a server error.
- Debug-only logging interceptor.
- A ViewModel unit test using a fake service (an object implementing
PostsApiService) that returns a short list, throws anIOException, or throws anHttpException. 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.
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) }
}
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.
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.
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”.
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.
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.
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