Chapter 1: Networking Fundamentals for Android
Every interesting app you have ever used talks to the internet. Instagram fetches your feed, WhatsApp delivers a message, your banking app checks your balance. None of that data lives on the phone permanently — it lives on a computer somewhere else, and the app's job is to go get it, show it to you, and sometimes send something back.
That "go get it and send something back" is networking, and it is the single most important skill separating a toy app from a real one. The good news: the fundamentals are simpler than they look. By the end of this chapter you will understand what actually happens when your app "talks to a server," and you will have a mental model solid enough that every line of Retrofit and Ktor in the rest of this book makes sense instead of feeling like magic.
We will not write any Retrofit or Ktor code yet. First, the concepts. Skipping them is exactly why so many junior developers copy-paste networking code they do not understand and then get stuck the moment something goes wrong.
What happens when you tap "Refresh"
Imagine you open Pulse — the chat app we are going to build throughout this book — and you tap on a channel to see its messages. Here is the chain of events, simplified:
- Your app decides it needs the messages for that channel.
- It writes a small text request that says, roughly, "Please give me the messages for channel 42."
- It sends that request over the internet to a specific computer — the server.
- The server reads the request, looks up the messages in its database, and writes a response.
- The response — a chunk of text containing the messages — travels back to your phone.
- Your app reads the response, turns it into Kotlin objects, and draws them on screen.
That is the whole loop. A request goes out, a response comes back. Almost everything in this book is a variation on those two steps. Even realtime chat, which feels different, is built on the same foundation with one important twist we will get to.
The client–server model
The two main characters in networking are the client and the server.
The client is the one who asks. Your Android app is a client. So is a web browser, or the Postman app you will use to poke at APIs later.
The server is the one who answers. It is a computer (or, realistically, hundreds of computers) running a program that waits for requests, does some work, and sends responses. When people say "the backend" or "the API," they usually mean the server.
A useful analogy is a restaurant. You (the client) look at a menu and tell the waiter what you want. The waiter carries your order to the kitchen (the server), which prepares it and sends it back. You do not need to know how the kitchen works — how the stove is lit, where the ingredients are stored — you only need to know how to place an order and what to expect on the plate. Networking is the same: your app needs to know how to place an order (build a request) and what the plate looks like (parse the response). What the server does internally is not your problem.
This separation is powerful. The same Pulse server can be talked to by an Android app, an iPhone app, and a website, all at once, because they all speak the same ordering language. That shared language is HTTP.
HTTP: the language of the web
HTTP (HyperText Transfer Protocol) is the set of rules that clients and servers use to talk. When your app talks to a server, it is almost always speaking HTTP. You do not have to implement HTTP yourself — libraries like OkHttp, Retrofit, and Ktor do the heavy lifting — but you absolutely need to understand its shape, because every one of those libraries is just a friendlier way to build HTTP requests and read HTTP responses.
An HTTP conversation is always one request followed by one response. The client speaks first, the server replies, and then that particular exchange is done. This is important: plain HTTP has no way for the server to speak up on its own. It can only answer when asked. Hold onto that thought — it is exactly the limitation that will push us toward realtime techniques like WebSockets later in the book.
Let's dissect both halves.
Anatomy of a request
Here is what an actual HTTP request looks like as raw text. This is your app asking the Pulse server for the messages in channel 42:
GET /v1/channels/42/messages HTTP/1.1
Host: api.pulse.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Accept: application/json
User-Agent: Pulse-Android/1.0
Every request has these pieces:
- The method (
GET) — what kind of action you want. More on methods in a moment. - The path (
/v1/channels/42/messages) — which resource on the server you are asking about. Notice42right there in the path; that is how we say which channel. - The protocol version (
HTTP/1.1) — you rarely think about this. - Headers — the lines after the first. Each is a
Name: Valuepair carrying extra information about the request. Here,Hostsays which server,Authorizationproves who you are, andAcceptsays "please answer in JSON." - A body (not shown here) — the actual data you are sending. A
GETusually has no body because you are only asking for data, not sending any. When you post a new message, the body carries the message text.
Anatomy of a response
The server reads that request and sends back something like this:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 213
{
"messages": [
{ "id": 1001, "author": "sara", "text": "Hey team!", "sentAt": "2026-06-01T09:15:00Z" },
{ "id": 1002, "author": "omar", "text": "Morning ☀️", "sentAt": "2026-06-01T09:16:12Z" }
]
}
A response has:
- A status line (
HTTP/1.1 200 OK) — the200is the status code, the server's one-number summary of how things went.200means success. - Headers — same idea as request headers.
Content-Type: application/jsontells your app "the body is JSON, parse it accordingly." - A body — the actual payload. Here it is the JSON list of messages your app will display.
That is genuinely all there is to the mechanics. Request out, response back, each made of a first line, some headers, and an optional body. Retrofit and Ktor exist to save you from typing that raw text by hand and from parsing it by hand — but under the hood, this is exactly what they are producing and reading.
HTTP methods: verbs for your intentions
The method is the verb of a request. It tells the server what kind of operation you intend. There are several, but as a junior you will use five constantly:
GET — "give me data." Fetch a channel, a list of messages, a user profile. A GET should never change anything on the server; asking twice should be as safe as asking once. This property is called being safe.
POST — "create something new." Send a new message, register a new account. POST typically carries a body with the data you are creating. Posting twice usually creates two things, so POST is not safe to blindly repeat.
PUT — "replace this thing entirely." Overwrite a user's whole profile with a new version.
PATCH — "update part of this thing." Change just the user's display name, leaving everything else alone.
DELETE — "remove this thing." Delete a message.
There is a subtle idea worth knowing early: idempotency. An operation is idempotent if doing it multiple times has the same effect as doing it once. GET, PUT, and DELETE are idempotent — deleting message 1001 twice leaves you in the same end state (it's gone). POST is not — posting the same message twice creates two messages. This matters a lot when a request fails and you are deciding whether it is safe to retry, which is exactly the kind of decision we will handle carefully in the error-handling and realtime chapters.
Status codes: the server's report card
Every response carries a three-digit status code. You do not need to memorize all of them, but you do need to recognize the families, because your app will behave differently for each. The first digit tells you the category:
2xx — Success. The request worked. 200 OK is the everyday success. 201 Created often comes back after a successful POST, meaning "I made the thing you asked for." 204 No Content means "success, but I have nothing to send back" — common after a DELETE.
3xx — Redirection. "The thing you want is somewhere else." You will rarely handle these by hand; the networking library usually follows redirects for you.
4xx — Client error. You did something wrong. This family is where you, the app developer, spend real time:
400 Bad Request— your request was malformed. Maybe you sent invalid JSON.401 Unauthorized— you are not logged in, or your token expired. In Pulse, this triggers a "please log in again" flow.403 Forbidden— you are logged in, but you are not allowed to do this.404 Not Found— the thing does not exist. Channel 999 was never created.429 Too Many Requests— you are asking too fast; slow down.
5xx — Server error. The server broke. 500 Internal Server Error and 503 Service Unavailable are the common ones. The important junior insight: a 5xx is usually not your fault and often worth retrying after a short wait. A 4xx is your fault and retrying the exact same request will fail exactly the same way.
Get comfortable with this split — "4xx means fix my request, 5xx means the server stumbled" — because it drives almost every decision about how your app reacts when something goes wrong.
Headers: the metadata that runs everything
Headers are Name: Value pairs that ride along with requests and responses, carrying information about the message rather than the message itself. A few you will meet constantly:
Content-Type— "the body is in this format."application/jsonis what we use throughout Pulse. When you upload a photo, it might beimage/jpeg. When you submit a web form,application/x-www-form-urlencoded.Accept— the request version of Content-Type: "please answer in this format."Authorization— how you prove who you are. UsuallyBearer <token>, where the token is a long string the server gave you when you logged in. We will build the full login-and-token flow in the security chapter.User-Agent— identifies the client.Pulse-Android/1.0lets the server know it is talking to our app.
Headers feel like housekeeping, but they are where authentication, content negotiation, and caching all live. A huge fraction of "why isn't my request working?" turns out to be a missing or wrong header.
JSON: how data is shaped
The server sent us messages, but in what form? Almost universally today, the answer is JSON (JavaScript Object Notation). JSON is a lightweight, human-readable text format for structured data, and it is the lingua franca between apps and servers.
JSON has only a handful of building blocks:
{
"id": 1001,
"author": "sara",
"text": "Hey team!",
"edited": false,
"reactions": ["👍", "🎉"],
"replyTo": null
}
- Objects — curly braces
{}holding"key": valuepairs. This whole message is an object. - Arrays — square brackets
[]holding an ordered list.reactionsis an array of strings. - Strings — text in double quotes:
"sara". - Numbers —
1001, no quotes. - Booleans —
trueorfalse. - null — the explicit absence of a value.
replyTois null because this message is not a reply.
Your app's job, once a JSON response arrives, is to turn that text into Kotlin objects you can actually use — a Message data class with an id, an author, a text, and so on. That translation is called deserialization (JSON → objects), and the reverse, when you send data, is serialization (objects → JSON). We will do this properly with kotlinx.serialization in Chapter 4. For now, just internalize that JSON is text, your Kotlin classes are objects, and a serialization library is the translator sitting between them.
HTTPS: the S is for secure
You may have noticed our server address starts with https://, not http://. That s stands for secure, and it matters enormously.
Plain HTTP sends everything as readable text across the network. Anyone positioned between your phone and the server — someone running the coffee-shop Wi-Fi, for instance — could read your messages and even your login token. That is obviously unacceptable for a chat app.
HTTPS wraps HTTP inside an encrypted tunnel using TLS (Transport Layer Security). Two things happen when your app connects over HTTPS: the data is encrypted so eavesdroppers see only scrambled noise, and the server's identity is verified through a certificate so you know you are really talking to api.pulse.example.com and not an impostor. You do not implement any of this — the networking stack handles it — but you must use it. Modern Android actively discourages plain HTTP, as we are about to see.
The Android-specific bits
Everything so far is true of networking anywhere. Now for the parts that are specifically Android, because these are where juniors hit their first walls.
You need permission to touch the internet
Android apps cannot access the network unless they declare that they intend to. In your AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application ... >
...
</application>
</manifest>
The INTERNET permission is a normal permission, meaning it is granted automatically at install time — the user is not prompted. But if you forget the line entirely, every network call fails immediately with a confusing error. It is the single most common "why doesn't my first request work?" mistake. Add it before anything else.
Android really wants you on HTTPS
Since Android 9 (API 28), apps block plain-text (non-HTTPS) traffic by default. If you try to hit an http:// URL, it fails unless you explicitly allow it. For production, you should be entirely on HTTPS, so this default protects you.
Occasionally during development you will point your app at a local test server running plain HTTP. For those cases you can add a network security configuration that permits cleartext only for specific domains:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
(That 10.0.2.2 is a special address the Android emulator uses to reach your development machine's localhost — a handy thing to remember.)
Then reference it in the manifest:
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
The rule of thumb: HTTPS everywhere in production, and only ever poke a cleartext hole for a named local domain during development.
You cannot do networking on the main thread
This is the big one, and it is the reason Chapter 2 exists.
Android apps have a single special thread called the main thread (or UI thread). It is responsible for drawing the screen and responding to taps. Every frame you see, every button press, runs on it. If that thread is ever busy for too long, the app freezes — nothing redraws, nothing responds — and if it stays frozen, Android shows the dreaded "App isn't responding" (ANR) dialog.
Networking is slow and unpredictable. A request might take 50 milliseconds on good Wi-Fi or 8 seconds on a train going through a tunnel. If you ran that request on the main thread, the entire UI would freeze for the whole duration, waiting. That is such a bad idea that Android forbids it outright: attempt a network call on the main thread and you get an immediate NetworkOnMainThreadException crash.
So networking must happen off the main thread, in the background, and then the result must be delivered back to the main thread to update the UI. Managing that "go do slow work elsewhere, then come back to update the screen" dance used to be genuinely painful. The modern Kotlin answer — coroutines — makes it clean and readable, and it is so central to networking on Android that we are dedicating the entire next chapter to it before we write a single Retrofit call.
Meet Pulse: the app we will build
Throughout this book we build one app, Pulse, a team chat application. Building one real thing across every chapter means each new concept has an obvious place to live, instead of arriving as a disconnected snippet.
Pulse has two sides that map perfectly onto the two halves of this book:
The request/response side — logging in, loading your list of channels, fetching a channel's message history, posting a new message, editing your profile. This is classic REST-over-HTTP, and it is what we will build with Retrofit (Part II) and then again with Ktor (Part III), so you can compare the two libraries doing the same jobs.
The realtime side — new messages from other people appearing instantly without you refreshing, "Omar is typing…" indicators, and green dots showing who is online right now. Plain request/response cannot do this well, because, remember, the server cannot speak unless spoken to. This is where WebSockets and friends come in (Part IV).
Here is a first look at the Pulse API contract. Do not worry about memorizing it; it is here so you know what we are aiming at. Every endpoint lives under the base URL https://api.pulse.example.com/v1.
POST /auth/login → log in, receive an auth token
POST /auth/refresh → exchange a refresh token for a fresh auth token
GET /users/me → the current user's profile
GET /channels → the list of channels you belong to
GET /channels/{id}/messages → a channel's message history
POST /channels/{id}/messages → send a new message to a channel
PATCH /users/me → update your own profile
WebSocket:
wss://api.pulse.example.com/v1/ws
← receive live events: new messages, typing, presence
→ send events: your own typing status
Notice the shapes you already recognize: methods (GET, POST, PATCH), paths with an {id} placeholder, an auth step that hands back a token, and — at the bottom — that wss:// WebSocket endpoint, the realtime piece that behaves completely differently from the rest. By the final chapter, you will have built every line of this.
A tool for seeing HTTP with your own eyes
You learn networking far faster when you can watch it happen. Before we move on, get familiar with at least one tool for making raw requests, so HTTP stops being abstract.
The quickest is curl, a command-line tool available on every Mac and Linux machine (and Windows too). This one line does exactly what our example request did:
curl -H "Accept: application/json" \
https://api.pulse.example.com/v1/channels/42/messages
The -H flag adds a header, and curl prints the raw response body right in your terminal. Add -i and it prints the status line and response headers too, so you can see the 200 OK and the Content-Type for yourself.
For something friendlier with a graphical interface, Postman lets you build requests by filling in fields and clicking Send. And once your app is running, a proxy tool like Charles or Proxyman can sit between your phone and the internet and show you every request your app makes — invaluable when you are debugging "why is my app sending that?"
Spend ten minutes firing a few GET requests at any public API you like. Watching the request go out and the JSON come back, live, is worth more than another page of explanation.
What you learned
You now have the mental model that the rest of this book stands on. Networking is a conversation between a client (your app) and a server (the backend), conducted in HTTP: a request goes out and a response comes back, each built from a first line, headers, and an optional body. Requests carry a method (GET, POST, PUT, PATCH, DELETE) declaring intent; responses carry a status code whose first digit — 2, 4, or 5 — tells you at a glance whether it worked, whether you broke it, or whether the server broke. Data flows as JSON, which a serialization library will translate to and from your Kotlin objects. Everything travels securely over HTTPS.
On the Android side specifically, you must declare the INTERNET permission, you should stay on HTTPS (adding a narrow network security config hole only for local development), and — most importantly — you can never run a network call on the main thread, because doing so freezes the UI and crashes with NetworkOnMainThreadException.
That last constraint is the doorway to the next chapter. Before we can make a single real call to a server, we need a clean, modern way to run slow work in the background and bring the result home to the UI. In Kotlin, that means coroutines and Flow — and that is exactly where we go next.
Exercises
- Using
curl -i(or Postman), make a GET request to any public JSON API — for examplehttps://api.github.com/users/google. Identify the status code, theContent-Typeheader, and the JSON body in the output. - From the same output, pick out three keys in the JSON and note their value types (string, number, boolean, array, object, or null).
- Write down, in your own words, what each of these status codes would mean if Pulse returned it when you tried to load a channel:
200,401,404,503. For each, decide whether retrying the identical request could plausibly help. - Explain to an imaginary teammate why
POST /channels/42/messagesis not idempotent butDELETE /channels/42/messages/1001is. - Look at the Pulse API contract and predict: which endpoints will send a request body, and which will not? Why?