COVID-19 All Countries Data in Android with Volley & RecyclerView (2026)
Last Updated: | Author: Deep Singh
In the previous tutorial, we fetched COVID-19 data for a single country. Now let's go bigger — fetch the data for every country in the world and display it as a scrollable grid of cards, each showing the country name and its total case count.
You'll learn how to parse a JSON array into a List<Case> with Gson, display it in a RecyclerView with GridLayoutManager, and open a dialog with more details when a card is tapped. This is the exact pattern you'll use for any list-driven Android screen — a list of products, users, articles, or anything else.
Heads-up on the API: The original 2021 tutorial used coronavirus-19-api.herokuapp.com, which was shut down when Heroku discontinued free dynos in November 2022. The code has been updated to use disease.sh, an active replacement that serves the same data.
Output Screen
Gradle Dependencies
Add these to your app-level build.gradle:
dependencies {
implementation 'com.android.volley:volley:1.2.1'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
}
What changed from the original:
- Volley upgraded from
1.1.0to1.2.1(bug fixes and security). - Gson upgraded from
2.8.5to2.11.0(the older version has known vulnerabilities). - RecyclerView upgraded from
1.1.0to1.3.2.
AndroidManifest.xml
The app needs internet access to fetch the API data.
<?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> for INTERNET. That element is meant for runtime permissions on API 23+ (like camera or location), not for install-time permissions like internet access. It should be a plain <uses-permission>.
Layouts
activity_main.xml
<?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="16dp">
<Button
android:id="@+id/fetchButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Load All Countries"
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" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myrecyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp" />
</LinearLayout>
case_item.xml — Each Card in the Grid
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/countryname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/countrycase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
What's new: The original post never showed the layouts at all — the tutorial jumped straight into Java. This version includes both the activity layout and the item layout, so you can build the app without guessing.
Case.java
This is the data model. Gson maps each JSON object in the response array to an instance of this class.
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; }
}
CustomCaseAdapter.java
This is the RecyclerView adapter. It binds each Case object to a card in the grid and forwards tap events through a listener interface.
package com.example.livejsonparsing;
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.List;
public class CustomCaseAdapter
extends RecyclerView.Adapter<CustomCaseAdapter.MyViewHolder> {
private final List<Case> caseList;
private CaseListener caseListener;
public CustomCaseAdapter(List<Case> caseList) {
this.caseList = caseList;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.case_item, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Case ca = caseList.get(position);
holder.tcountry.setText(ca.getCountry());
holder.tcase.setText("Cases: " + ca.getCases());
}
@Override
public int getItemCount() {
return caseList.size();
}
public void setCaseListener(CaseListener listener) {
this.caseListener = listener;
}
public interface CaseListener {
void caseItem(View view, int position);
}
class MyViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
TextView tcountry, tcase;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
tcountry = itemView.findViewById(R.id.countryname);
tcase = itemView.findViewById(R.id.countrycase);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (caseListener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
caseListener.caseItem(view, position);
}
}
}
}
}
What's been fixed from the original:
- Removed the stray
Log.ecall. The original calledLog.e("error", caseList.size() + "")insideonBindViewHolder— that fires on every scroll and floods the logcat. Removed. - Removed
android.util.Logimport. No longer needed. - Added null check for
caseListener. If the listener is never set, tapping a card would crash with aNullPointerException. Now it safely does nothing. - Used
getBindingAdapterPosition(). The original used the deprecatedgetAdapterPosition(). The new method handles the case where the item has been removed and returnsNO_POSITION. - Better display text. Shows "Cases: 12345" instead of just the raw number, which is much more readable.
- Made
caseListfinal. It's assigned once in the constructor and never replaced. - Fixed the `onCreateViewHolder` continuation — the original had a stray line break that made it look like the code was split across two lines when it wasn't.
MainActivity.java
package com.example.livejsonparsing;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
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;
import com.google.gson.reflect.TypeToken;
import org.json.JSONArray;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ProgressBar progressBar;
private RecyclerView recyclerView;
private CustomCaseAdapter customCaseAdapter;
private RequestQueue requestQueue;
private final List<Case> caseList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = findViewById(R.id.progress);
recyclerView = findViewById(R.id.myrecyclerview);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
customCaseAdapter = new CustomCaseAdapter(caseList);
recyclerView.setAdapter(customCaseAdapter);
requestQueue = Volley.newRequestQueue(this);
customCaseAdapter.setCaseListener(new CustomCaseAdapter.CaseListener() {
@Override
public void caseItem(View view, int position) {
Case ca = caseList.get(position);
new AlertDialog.Builder(MainActivity.this)
.setTitle(ca.getCountry())
.setMessage(
"Total Cases : " + ca.getCases() + "\n" +
"Today Cases : " + ca.getTodayCases() + "\n" +
"Total Deaths : " + ca.getDeaths() + "\n" +
"Today Deaths : " + ca.getTodayDeaths() + "\n" +
"Recovered : " + ca.getRecovered() + "\n" +
"Active : " + ca.getActive() + "\n" +
"Critical : " + ca.getCritical()
)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (requestQueue != null) {
requestQueue.cancelAll(this);
}
}
public void getData(View view) {
progressBar.setVisibility(View.VISIBLE);
caseList.clear();
customCaseAdapter.notifyDataSetChanged();
String url = "https://disease.sh/v3/covid-19/countries";
StringRequest sr = new StringRequest(
Request.Method.GET,
url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
progressBar.setVisibility(View.GONE);
try {
JSONArray jsonArray = new JSONArray(response);
Gson gson = new Gson();
for (int i = 0; i < jsonArray.length(); i++) {
Case cs = gson.fromJson(
jsonArray.getJSONObject(i).toString(),
Case.class
);
if (cs != null) {
caseList.add(cs);
}
}
customCaseAdapter.notifyDataSetChanged();
} catch (Exception e) {
Log.e("Parse", "Failed to parse response", e);
Toast.makeText(MainActivity.this,
"Failed to parse data",
Toast.LENGTH_SHORT).show();
}
}
},
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);
requestQueue.add(sr);
}
}
What's been fixed and improved:
- Removed the unused
AsyncTaskimport. The original importedandroid.os.AsyncTaskbut never used it. - Removed the unused
TextViewimport. No TextView is used in MainActivity. - Removed the unused
Type/TypeTokenimports. The original didn't use Gson's type tokens. But I've added a cleaner version below using them, so the imports are actually correct now. - Reused the RequestQueue. The original created a new one on every tap:
Volley.newRequestQueue(MainActivity.this).add(sr). That leaks threads. Now it's a single field created inonCreate. - Cancelled pending requests in `onDestroy`. Prevents callbacks from firing on a destroyed activity.
- Cleared the list before repopulating. If the user taps the button twice, the list is reset so you don't get duplicate rows.
- Wrapped JSON parsing in try/catch. If the API changes or returns malformed data, the app shows a toast instead of crashing.
- Switched to the new `disease.sh` API. The original endpoint is dead.
- Better dialog. The original only showed the death count. Now it shows every field the API provides.
- Consistent message text. The original mixed "Total Death" (singular) with "Total Cases". Standardised everything.
A Cleaner Alternative: Parse the Whole Array with Gson
If you prefer, you can skip the manual JSONArray loop entirely and let Gson parse the whole list in one line using a TypeToken:
Gson gson = new Gson();
Type listType = new TypeToken<List<Case>>(){}.getType();
List<Case> parsed = gson.fromJson(response, listType);
caseList.clear();
caseList.addAll(parsed);
customCaseAdapter.notifyDataSetChanged();
This is cleaner and faster — Gson knows how to build a List<Case> directly from the JSON array. The TypeToken is necessary because Java erases generics at runtime, so Gson needs to know the element type at parse time.
I've kept the manual JSONArray loop in the main example because the original tutorial used it — but the TypeToken version is what you'd write in production.
How the Code Works
Let's trace the flow when the user taps "Load All Countries".
1. Setup in onCreate
We wire up the RecyclerView with a GridLayoutManager set to 2 columns. This is what makes the output a grid rather than a vertical list. We also create the adapter, attach the click listener, and build the single RequestQueue.
2. Button Tap Fires getData()
The android:onClick="getData" attribute in the layout wires the button directly to the method. We show a spinner, clear the list, and start the request.
3. Volley Fetches the JSON Array
Volley sends a GET request to https://disease.sh/v3/covid-19/countries. The response is a JSON array with one object per country.
4. Gson Parses Each Object
We wrap the response in a JSONArray, loop through it, and use gson.fromJson() on each element to produce a Case object. This builds up the caseList one object at a time.
5. Adapter Refreshes the Grid
After the loop, we call notifyDataSetChanged(). RecyclerView uses the adapter's getItemCount() and onBindViewHolder() to render each country as a card in the grid.
6. Card Tap Opens a Dialog
The adapter forwards tap events through its CaseListener interface. In MainActivity, the listener implementation reads the tapped Case object and shows an AlertDialog with all its fields.
What the API Returns
For https://disease.sh/v3/covid-19/countries, the response looks like:
[
{
"country": "India",
"cases": 45000000,
"todayCases": 1200,
"deaths": 530000,
"todayDeaths": 15,
"recovered": 44500000,
"active": 24000,
"critical": 700,
...
},
{
"country": "USA",
"cases": 100000000,
...
},
...
]
Common Pitfalls
- Assuming the API endpoint will stay alive. The original tutorial's endpoint died when Heroku ended free dynos in 2022. If your app suddenly stops working, check the endpoint first.
- Creating a new RequestQueue on every request. This leaks threads and network connections. Use a single queue per activity (or a singleton queue for the whole app).
- Not cancelling requests on activity destroy. If the user leaves the screen while a request is in-flight, the callback fires on a destroyed activity. Use
setTag()+cancelAll(). - Forgetting to clear the list. If the user taps the fetch button twice,
caseListdoubles in size. Always clear it before repopulating. - Adding a log call inside `onBindViewHolder`. It fires on every scroll and floods Logcat. Only log when something is actually wrong.
- Using the deprecated `getAdapterPosition()`. Use
getBindingAdapterPosition()instead. It handles the case where the item has been removed from the list. - Null case listener. If
setCaseListeneris never called, tapping a card crashes on a null pointer. Always check for null before calling. - Assuming a `uses-permission-sdk-23` is right for INTERNET. That element is only for runtime permissions. INTERNET is granted at install time with a plain
uses-permission. - Not handling non-200 responses. Volley's error listener fires for network errors, but a 500 response with valid body content still goes to
onResponse. Consider checkingresponsecontent or using aJsonObjectRequestwith a status code check. - Parsing with `int` when the API might return `long`. Case counts for large countries can exceed 2 billion. Consider using
longinstead ofintto avoid overflow on the largest values. - Sending too many items to `notifyDataSetChanged()`. For large lists (200+ countries), that's fine, but for thousands of items, use
DiffUtilorListAdapterfor smooth updates.
Wrapping Up
You now have a working Android app that fetches data for every country from a live API, parses it with Gson, and displays it in a scrollable grid of cards. Tapping a card opens a dialog with detailed stats.
From here, you can extend the example with:
- Search box. Add an
EditTextabove the grid to filter countries as the user types. - Sort options. Sort by name, total cases, or deaths using a menu or spinner.
- Pull to refresh. Wrap the RecyclerView in a
SwipeRefreshLayoutto let users reload. - Detail screen instead of dialog. Replace the AlertDialog with a full activity showing charts and history.
- Offline caching. Store the last response in Room and show it when the network fails.
- Retrofit + Moshi. Retrofit is the modern standard for Android HTTP requests and pairs well with Moshi (a Gson alternative with better Kotlin support).
- Coroutines. If you migrate to Kotlin, replace the Volley callbacks with suspend functions and Kotlin coroutines.
6 comments:
Very good written article. It will be supportive to anyone who utilizes it, including me. Keep doing what you are doing – can't wait to read more posts.the complete guide
I am so much excited after reading your blog. Your blog is very much innovative and much helpful for any industry as well as in person.
flutter programmierer
Being cute is a way of life for this lively charmer. The Shih Tzu is known to be especially affectionate with children. As a small dog bred to spend most of their day inside royal palaces, they make a great pet if you live in an apartment or lack a big backyard.
where to buy Shih Tzu puppies
We offer great deals and expert service.Our specialty are on gun brands like the COLT,which include,the colt rifles,revolvers,colt pistols,Taurus Handguns and Panzer arms.We have the latest collection of the AR-12 Shotguns,panzer bp-12,panzer arms ar12 pro. USA GUN FACTORY is a federally licensed firearms dealer, Visit now
Taurus Tracker
I seriously love your blog.. Pleasant colors & theme.
Did you build this amazing site yourself? Please reply back as I’m trying to create my own personal blog and would like to
learn where you got this from or what the theme is named. Many thanks!
order muha meds carts online
I quite like reading through an article that can make men and women think.
Also, many thanks for allowing for me to comment!
Buy Vyvanse Online cheap and affordable with or without prescription
Post a Comment