COVID-19 Tracker by Country in Android: Volley + Gson Tutorial (2026)
Last Updated: | Author: Deep Singh
Fetching live data from a REST API and displaying it in an Android app is a skill you'll use over and over — for weather apps, stock tickers, sports scores, and more. In this tutorial, you'll learn how to do exactly that with two of the most common Android libraries: Volley for HTTP requests and Gson for JSON parsing.
We'll build a COVID-19 tracker that lets users fetch current case data for any country. The app is small, but it demonstrates the full flow: make a request, parse JSON, bind it to a data class, and display it in the UI.
Heads-up on the API: The original 2021 tutorial used coronavirus-19-api.herokuapp.com, which has been shut down (Heroku discontinued free dynos in November 2022). The code has been updated to use disease.sh, a currently-active replacement that serves the same data in a similar format.
Gradle Dependencies
In your app-level build.gradle, add:
dependencies {
implementation 'com.android.volley:volley:1.2.1'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'com.google.android.material:material:1.12.0'
}
What changed from the original:
- Volley upgraded from
1.1.0to1.2.1— the version in the original tutorial has known bugs and lacks recent fixes. - Gson upgraded from
2.8.5to2.11.0— the older version has security vulnerabilities. - Added Material for a modern button style.
AndroidManifest.xml
The app needs internet access. Add the permission above the <application> tag.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.livejsonparsing">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Fix: The original used <uses-permission-sdk-23>, which is wrong for the INTERNET permission — that element is only meant for permissions that must be requested at runtime on API 23+. INTERNET is a normal permission that's granted at install time. Use <uses-permission>.
Modern note: Starting with Android 9, cleartext HTTP traffic is blocked by default. Since our API uses HTTPS, no extra configuration is needed. If you ever switch to an HTTP endpoint, you'd need to add android:usesCleartextTraffic="true" or a network security config.
activity_main.xml
A simple layout with a button to trigger the fetch, a progress bar, and a text view for the result.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<Button
android:id="@+id/fetchButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fetch COVID-19 Data"
android:onClick="getData" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:visibility="gone" />
<TextView
android:id="@+id/mydata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:textSize="18sp"
android:lineSpacingExtra="6dp" />
</LinearLayout>
What's new: The button has android:onClick="getData", which means the getData() method in MainActivity is called when the button is tapped — no need for a separate setOnClickListener. This matches how the original getData(View view) signature was set up.
Case.java — The Data Model
This class mirrors the JSON structure returned by the API. Gson uses it to convert the JSON response into a Java object automatically.
package com.example.livejsonparsing;
public class Case {
private String country;
private int cases;
private int todayCases;
private int deaths;
private int todayDeaths;
private int recovered;
private int active;
private int critical;
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public int getCases() { return cases; }
public void setCases(int cases) { this.cases = cases; }
public int getTodayCases() { return todayCases; }
public void setTodayCases(int todayCases) { this.todayCases = todayCases; }
public int getDeaths() { return deaths; }
public void setDeaths(int deaths) { this.deaths = deaths; }
public int getTodayDeaths() { return todayDeaths; }
public void setTodayDeaths(int todayDeaths) { this.todayDeaths = todayDeaths; }
public int getRecovered() { return recovered; }
public void setRecovered(int recovered) { this.recovered = recovered; }
public int getActive() { return active; }
public void setActive(int active) { this.active = active; }
public int getCritical() { return critical; }
public void setCritical(int critical) { this.critical = critical; }
}
Important: The field names must match the JSON keys exactly. Gson matches by field name (case-sensitive by default), so if the API returns "todayCases", the field must be named todayCases — not todaycases or today_cases.
MainActivity.java — Fetch and Display
package com.example.livejsonparsing;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import com.google.gson.Gson;
public class MainActivity extends AppCompatActivity {
private ProgressBar progressBar;
private TextView tdata;
private RequestQueue requestQueue;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = findViewById(R.id.progress);
tdata = findViewById(R.id.mydata);
requestQueue = Volley.newRequestQueue(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (requestQueue != null) {
requestQueue.cancelAll(this);
}
}
public void getData(View view) {
final String country = "india";
progressBar.setVisibility(View.VISIBLE);
tdata.setText("");
String url = "https://disease.sh/v3/covid-19/countries/" + country;
StringRequest sr = new StringRequest(
Request.Method.GET,
url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
progressBar.setVisibility(View.GONE);
try {
Gson gson = new Gson();
Case cc = gson.fromJson(response, Case.class);
if (cc != null) {
String output =
"Country : " + cc.getCountry() + "\n" +
"Active Cases : " + cc.getActive() + "\n" +
"Total Cases : " + cc.getCases() + "\n" +
"Today Cases : " + cc.getTodayCases() + "\n" +
"Total Deaths : " + cc.getDeaths() + "\n" +
"Today Deaths : " + cc.getTodayDeaths() + "\n" +
"Recovered : " + cc.getRecovered() + "\n" +
"Critical : " + cc.getCritical();
tdata.setText(output);
} else {
tdata.setText("No data returned for " + country);
}
} catch (Exception e) {
Log.e("Parse", "Failed to parse response", e);
tdata.setText("Failed to parse data");
}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
progressBar.setVisibility(View.GONE);
Log.e("Volley", "Request failed", error);
Toast.makeText(MainActivity.this,
"Error: " + error.getMessage(),
Toast.LENGTH_SHORT).show();
}
}
);
sr.setTag(this); // For cancellation in onDestroy
requestQueue.add(sr);
}
}
What's been fixed from the original:
- Reused the RequestQueue. The original created a new queue on every request:
Volley.newRequestQueue(MainActivity.this).add(sr). That leaks resources. A single queue created once inonCreate()is the correct pattern. - Cancelled pending requests in `onDestroy`. If the user closes the activity while a request is in-flight, the callback could fire on a destroyed activity. Setting a tag and cancelling by tag prevents this.
- Added a try/catch around Gson parsing. If the API returns something unexpected, the app shows an error instead of crashing.
- Cleared the TextView before fetching. Otherwise, old data lingers on screen while the new request is loading.
- Switched API endpoint. The original
coronavirus-19-api.herokuapp.comwas shut down. The new endpointdisease.sh/v3/covid-19/countries/serves the same data. - Upgraded Volley and Gson versions. The originals have known bugs and security issues.
- Fixed the `uses-permission-sdk-23` misuse. That element is for runtime permissions, not INTERNET.
- Improved display formatting. One field per line, clearer labels, and consistent naming.
- Handled the `null` case. If
gson.fromJsonreturns null (malformed JSON), the app shows a message instead of crashing on thecc.getCountry()call.
How the Code Works
Let's walk through the flow when the user taps "Fetch COVID-19 Data".
1. Button Click Fires getData()
The android:onClick="getData" attribute in the layout wires the button directly to the getData(View) method in the activity.
2. UI Shows Loading State
The progress bar becomes visible, and the result TextView is cleared. This gives immediate feedback that something is happening.
3. Volley Makes the HTTP Request
StringRequest is a Volley helper for making a request where the response is a plain string (which we'll then parse as JSON). We pass the URL, a success listener, and an error listener.
Volley handles threading for us — the request runs on a background thread, and the callback fires on the main thread, so it's safe to update the UI directly.
4. Gson Parses the JSON
gson.fromJson(response, Case.class) takes the raw JSON string and produces a populated Case object. Gson matches JSON keys to Java fields by name, so "country" goes to the country field, "todayCases" to todayCases, and so on.
5. UI Updates
The parsed data is formatted into a multi-line string and displayed in the TextView. The progress bar is hidden.
6. Errors Are Handled
If the network request fails, the error listener fires. We log the error and show a toast to the user. The progress bar is hidden either way.
What the API Returns
For a request to https://disease.sh/v3/covid-19/countries/india, the response looks like:
{
"country": "India",
"cases": 45000000,
"todayCases": 1200,
"deaths": 530000,
"todayDeaths": 15,
"recovered": 44500000,
"active": 24000,
"critical": 700,
...
}
Gson maps this directly onto the Case class — as long as field names match.
Common Pitfalls
- Assuming the API endpoint is permanent. Free APIs disappear. The original tutorial's endpoint (
coronavirus-19-api.herokuapp.com) was shut down when Heroku discontinued free dynos. If your app suddenly stops working, check the API first. - Creating a new RequestQueue on every request. This leaks threads and network connections. Create one queue per activity (or per app, in a singleton) and reuse it.
- Not cancelling requests on activity destroy. If a request finishes after the user has navigated away, the callback fires on a destroyed activity, potentially causing a crash or a memory leak.
- Parsing without try/catch. If the API changes its response shape or returns an error page,
gson.fromJsonthrows. Wrap it in a try/catch. - Gson field name mismatches. Gson is case-sensitive by default. If the JSON key is
"todayCases"and your field istoday_cases, Gson silently leaves it as 0. Use@SerializedNameif names must differ. - Assuming integer fields. If a field in the JSON is sometimes a number and sometimes a string (like
null), Gson throws when trying to assign it to anint. Consider usingInteger(boxed) orlongfor large numbers like case counts. - Using `uses-permission-sdk-23` for INTERNET. That element is for runtime permissions on API 23+. INTERNET is a normal permission that goes in a plain
uses-permissiontag. - Not displaying a loading state. Users tap the button and nothing happens for a second. Showing a progress bar fixes this.
- Hard-coding the country. The original hardcodes
"india". In a real app, add an EditText so the user can type any country. - Volley's default retry policy. Volley retries failed requests once by default. For APIs that charge per request, this can double your bill. Set a custom
RetryPolicyif needed.
Wrapping Up
You now know how to fetch live JSON data from a REST API in Android, parse it with Gson, and display it in a simple UI using Volley. This pattern — request → parse → bind → display — is the foundation of almost every data-driven Android app.
From here, you can extend the example with:
- User input. Replace the hardcoded
"india"with anEditTextso the user can search any country. - RecyclerView of countries. Fetch the full list from
/v3/covid-19/countriesand display it in a scrolling list. - Room database caching. Store the last fetched data locally so the app shows something when offline.
- Retrofit instead of Volley. Retrofit is the modern standard for Android HTTP requests, and it works beautifully with Gson or Moshi.
- Coroutines + Kotlin. If you're migrating to Kotlin, Volley callbacks are replaced by suspend functions and coroutines.
- Error UI. Instead of a toast, show an inline error state with a retry button.
No comments:
Post a Comment