Android Data, Networking & APIs

Consuming a REST API

18 min Lesson 8 of 12

Consuming a REST API

In the previous two lessons you set up HttpURLConnection and configured Retrofit. Now you will put everything together end-to-end: fire a real network request, parse the JSON response, and render the data inside a RecyclerView — all correctly threaded so the UI never freezes. This is the pattern that powers almost every production Android app that talks to a backend.

The Goal: A List of GitHub Repositories

We will call the public GitHub API endpoint GET https://api.github.com/users/{username}/repos, which returns a JSON array of repository objects. Each object has (among other fields) a name, a description, and a stargazers_count. No authentication is required for public data, making it a perfect learning target.

Step 1 — Add the Model Class

Create a plain Java class that mirrors the JSON fields you care about. Retrofit (via Gson) will populate it automatically.

// GitHubRepo.java public class GitHubRepo { private String name; private String description; private int stargazers_count; public String getName() { return name; } public String getDescription() { return description; } public int getStars() { return stargazers_count; } }
Field names must match JSON keys exactly when using Gson's default mapping. If you want a different Java name, annotate with @SerializedName("stargazers_count"). Here the field is named identically, so no annotation is needed.

Step 2 — Define the Retrofit Interface

One method per endpoint. The @Path annotation replaces the {username} placeholder in the URL at runtime.

import java.util.List; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; public interface GitHubService { @GET("users/{username}/repos?per_page=30&sort=stars") Call<List<GitHubRepo>> getRepos(@Path("username") String username); }

Step 3 — Build the Retrofit Singleton

Create a one-time instance that you reuse across the app. Typically this lives in an ApiClient class or a dependency-injection provider.

import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ApiClient { private static final String BASE_URL = "https://api.github.com/"; private static Retrofit instance; public static Retrofit getInstance() { if (instance == null) { instance = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } return instance; } }

Step 4 — Wire Up the RecyclerView

Create a minimal adapter that binds a list of GitHubRepo objects to item views.

import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.ViewHolder> { private List<GitHubRepo> repos = new ArrayList<>(); public void setRepos(List<GitHubRepo> repos) { this.repos = repos; notifyDataSetChanged(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_2, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { GitHubRepo repo = repos.get(position); holder.name.setText(repo.getName()); String desc = repo.getDescription() != null ? repo.getDescription() : "No description"; holder.detail.setText(desc + " ★ " + repo.getStars()); } @Override public int getItemCount() { return repos.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView name, detail; ViewHolder(View v) { super(v); name = v.findViewById(android.R.id.text1); detail = v.findViewById(android.R.id.text2); } } }

Step 5 — Fire the Request in the Activity

This is where the pieces come together. Call.enqueue() dispatches the HTTP request on a background thread managed by Retrofit, then delivers the result on the main thread — no AsyncTask or manual Handler needed.

import android.os.Bundle; import android.util.Log; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class RepoListActivity extends AppCompatActivity { private static final String TAG = "RepoListActivity"; private RepoAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_repo_list); RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new RepoAdapter(); recyclerView.setAdapter(adapter); fetchRepos("torvalds"); // replace with any GitHub username } private void fetchRepos(String username) { GitHubService service = ApiClient.getInstance() .create(GitHubService.class); Call<List<GitHubRepo>> call = service.getRepos(username); call.enqueue(new Callback<List<GitHubRepo>>() { @Override public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) { if (response.isSuccessful() && response.body() != null) { adapter.setRepos(response.body()); } else { Log.e(TAG, "HTTP " + response.code()); Toast.makeText(RepoListActivity.this, "Error: " + response.code(), Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call<List<GitHubRepo>> call, Throwable t) { Log.e(TAG, "Network failure", t); Toast.makeText(RepoListActivity.this, "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } }
Always check response.isSuccessful() before accessing the body. A 404 or 403 response is technically a successful HTTP transaction — Retrofit calls onResponse for any completed request. Only a thrown exception (no connectivity, timeout, SSL error) triggers onFailure. Ignoring the status code means silently swallowing API errors.

Understanding the Callback Thread Contract

Retrofit's OkHttp client runs on its own thread pool. When the response arrives, Retrofit posts the callback to the main (UI) thread automatically — you can update Views directly inside onResponse and onFailure without any extra thread switching. This is why the adapter call in the example above works without a runOnUiThread() wrapper.

Cancelling In-Flight Requests

If the user navigates away before the response arrives, the callback should not update a destroyed Activity. Hold a reference to the Call object and cancel it in onDestroy.

private Call<List<GitHubRepo>> currentCall; private void fetchRepos(String username) { GitHubService service = ApiClient.getInstance().create(GitHubService.class); currentCall = service.getRepos(username); currentCall.enqueue(/* ... same callback ... */); } @Override protected void onDestroy() { super.onDestroy(); if (currentCall != null) { currentCall.cancel(); } }
Memory leaks and crashes. An anonymous Callback holds an implicit reference to the enclosing Activity. If the Activity is destroyed (rotation, back press) while the call is in-flight, the callback fires on a dead object. Cancelling in onDestroy prevents this. In a production app you would move the call into a ViewModel so it survives rotation — but cancellation is still the baseline safety net.

Required Manifest Permission

Without this line in AndroidManifest.xml every network request silently fails with a SecurityException or returns an empty response:

<uses-permission android:name="android.permission.INTERNET" />

Because INTERNET is a normal (not dangerous) permission, the user is never prompted — you just declare it and the OS grants it at install time.

Summary

Consuming a REST API in Android comes down to five repeatable steps: model the JSON as a POJO, describe the endpoint in a Retrofit interface, build the client once, configure a RecyclerView adapter, and call enqueue() with a Callback. Retrofit handles threading and deserialization for you; your job is to check the status code, update the UI on success, and cancel the call when the screen is gone. This pattern scales from a single endpoint to a full multi-service app without changing the core structure.