Overview
The most exciting part of this application is to display the wide and enormous amount of data of all the infected countries in one single page using the most effective approach to persuade the audience. That approach includes the 'RecyclerView' element of android.
Statistics or Cases by Countries page of Covid-19 application serves multiple purposes. One of the most important purposes is to display the total number of cases of each country. Other purposes include displaying active, death, recovered, new cases, and new deaths of each country.
As mentioned in my previous blog, this application calling the REST API to extract the data. For this single page, we are talking about an enormous amount of data which includes a list of countries and case data associated with each country.
Let's break this page into components. The first component would be the list of cards which includes country base data. To create this component, we would need to implement a Recycler view. We also need two different classes: data provider and adapter. We need one class that would provide data in a correct format and one adapter class that binds the data in a recycler view.
The second component is a search bar with an ability to search the recycler view for a specific country and the ability to clear the form. This is a valuable component of the user interface standpoint.
The rest of the components like Header and Navigation Bar has already been covered in the previous blog of this series. You can refer them here.
So let's get started and create an insightful recycler view.
Code
Since we have already set up the DataService class in the previous blog, therefore, we can move ahead and start by creating a UI for this page. As you might know, considering that you read my previous blog, that I don't like to explain UI because it's fairly easy. So I am linking my UI code down below which you can use to set up the UI.
I have used Constraint Layout for this page as a default, but I have also added Recycler View to implement the list functionality on the page.
Once you implement the UI, the page would look like this
Now we can move on to two very important classes.
ListCasesDataProvider.java
This is class is all about getters and setters. It is always very important to figure out all the variables that we will be dealing with later and create getters and setters for those variables to establish control over the goal we are trying to achieve. In our case, we need to list all the variables that are in the UI page like country name, active cases, recovered cases, new cases, e.t.c.
So let's start by creating variables for all the data fields.
private String countryName;
private String cases;
private String activeCases;
private String recoveredCases;
private String deaths;
private String newCases;
private String newDeaths;
private String seriousCritical;
private String casesPerMillion;
private boolean expanded;
To set getters and setter for all these variables, highlight all the variables, and generate getters and setters. Now let's create a constructor that will allow us to set all these variables when we initialize this class in a foreign class.
public ListCasesDataProvider(String countryName, String cases, String activeCases,
String recoveredCase, String deaths, String newCases, String newDeaths, String seriousCritical,
String casesPerMillion)
{
this.countryName = countryName;
this.cases = cases;
this.activeCases = activeCases;
this.recoveredCases = recoveredCase;
this.deaths = deaths;
this.newCases = newCases;
this.newDeaths = newDeaths;
this.seriousCritical = seriousCritical;
this.casesPerMillion = casesPerMillion;
this.expanded = false;
}
You might be wondering how and when will we assign values to these variables. I don't believe in suspense so I'll tell you now. We will set values to these variables inside our main entry point CasesByCountry.java class. We extract data from the DataService class inside the main class and assign those values to these variables.
ListCasesAdapter.java
Alright, the time has come to write code for the heart of this page. This class binds data from the data provider class to the UI fields. This class serves an important purpose for recycler view because the list of county names from data providers will decide the number of country cards in recycler view. So, this adapter has to work.
Let's get started by importing all the necessary dependencies.
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
As I mentioned above that this class serves as an adapter for recycler view, and luckily there is already an adapter that we can use to implement all the necessary methods in the class.
public class ListCasesAdapter extends RecyclerView.Adapter<ListCasesAdapter.ViewHolder>
{
We need to implement ListCasesDataProvider class in the form of List because the data from the data provider class will be used to determine the position of the country card in recycler view and to determine the size of the recycler view list as well.
private List<ListCasesDataProvider> listItems;
Another variable we need is Context. We need to supply the context of cases by country page to ViewHolder so that recycler view can fir perfectly on the page.
private Context context;
Create the constructor for the adapter class using the variable mentioned above.
public ListCasesAdapter(List<ListCasesDataProvider> listItems, Context context)
{
this.listItems = listItems;
this.context = context;
}
Let's create a method that can serve as a view holder for the recycler view list. The main purpose of this method would be to assign UI variables which can later be used to set data in the form of a text.
Extend our custom ViewHolder with recycler view's view holder.
public class ViewHolder extends RecyclerView.ViewHolder{
Create appropriate variables for the UI text fields.
public TextView textViewHead, newCases, newDeaths, seriousCritical, casesPerMillion;
public TextView textViewDescription, activeCases, recoveredCases, deaths;
public LinearLayout linearLayout;
ConstraintLayout expandableLayout;
Now we can assign the UI variables to the variables we create above in the form of a constructor.
public ViewHolder(@NonNull View itemView)
{
super(itemView);
textViewHead = (TextView) itemView.findViewById(R.id.textViewHead);
textViewDescription = (TextView) itemView.findViewById(R.id.textViewDescription);
activeCases = (TextView) itemView.findViewById(R.id.activeCases);
recoveredCases = (TextView) itemView.findViewById(R.id.recoveredCases);
deaths = (TextView) itemView.findViewById(R.id.deaths);
newCases = (TextView) itemView.findViewById(R.id.todayCases);
newDeaths = (TextView) itemView.findViewById(R.id.todayDeaths);
seriousCritical = (TextView) itemView.findViewById(R.id.seriousCritical);
casesPerMillion = (TextView) itemView.findViewById(R.id.casesPerMillion);
linearLayout = (LinearLayout) itemView.findViewById(R.id.linearLayout);
expandableLayout = itemView.findViewById(R.id.expandableLayout);
textViewHead.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ListCasesDataProvider ls = listItems.get(getAdapterPosition());
ls.setExpanded(!ls.isExpanded());
notifyItemChanged(getAdapterPosition());
}
});
}
There is click listener on header which allows to expand the card in the recycler view that contains all the other necessary information or data about country.
Now we can bind the data listCaseDataProvider class to the variable we created inside View Holder. As those variables are assigned to UI variable therefore, setting text View Holder variables will set the text to UI variables on the page.
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position)
{
ListCasesDataProvider listItem = listItems.get(position);
holder.textViewHead.setText(listItem.getCountryName());
holder.textViewDescription.setText(listItem.getCases());
holder.activeCases.setText(listItem.getActiveCases());
holder.recoveredCases.setText(listItem.getRecoveredCases());
holder.deaths.setText(listItem.getDeaths());
holder.newCases.setText(listItem.getNewCases());
holder.newDeaths.setText(listItem.getNewDeaths());
holder.seriousCritical.setText(listItem.getSeriousCritical());
holder.casesPerMillion.setText(listItem.getCasesPerMillion());
boolean isExpanded = listItems.get(position).isExpanded();
holder.expandableLayout.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
}
If the card is expanded then we are setting the expandable layout view (child view) to be visible, but if it is not expanded then we are keeping the expandable layout to be gone.
This is the expandable layout view which we are set to be VISIBLE or GONE when the user clicks on the header of each card:
Now we can write the override method for recycler view adapter. The first method we are going to override is onCreateViewHolder. This method is mainly responsible to add recycler view at the right spot on the page given the context.
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
return new ViewHolder(v);
}
There is one more method left to override and that is getItemCount. This method is responsible to tell the adapter how many items there are in the recycler view which will be determined using the size of items we received from the listCasesDataProvider class.
@Override
public int getItemCount() {
return listItems.size();
}
And that's it for the adapter. The adapter is the core of this page so it is very crucial to understand all the pieces inside the class. The adapter takes the data from the custom DataService class and binds it with UI using Recycler view. Now that we have everything set up, we can move ahead towards creating the main entry point of this page. The main entry point will tie up all the work that we have done so far.
CasesByCountry.java
This class serves as the main entry point for adapter and data provider classes that we will work on later in this blog. For starters, add all these imports in a class that we will need to write code in this class.
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import org.json.JSONException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
First thing first, start by initializing all the variables that you will need to write back-end for the page. Of course, you will not know all the variables all the time ahead of the time. But since this is a tutorial so go ahead and create these variables.
Start by most obvious ones: UI variables:
private ProgressBar progressBar;
ImageButton btnRefresh, btnSearch, btnClear;
EditText searchedText;
Now create all the variables that will be needed to implement recycler view which includes adapters and data-provider class.
private RecyclerView recyclerView;
private RecyclerView.Adapter adapter;
private List<ListCasesDataProvider> listItems;
private List<ListCasesDataProvider> listItems2;
Context con = null;
We also need to initialize list variables for the data service class.
ArrayList<String> countryNames = new ArrayList<>();
ArrayList<String> resultSet = new ArrayList<>();
List<String> cases = new ArrayList<>();
List<String> deaths = new ArrayList<>();
List<String> totalRecovered = new ArrayList<>();
List<String> activeCases = new ArrayList<>();
List<String> newCases = new ArrayList<>();
List<String> newDeaths = new ArrayList<>();
List<String> seriousCritical = new ArrayList<>();
List<String> totalCasesPerMillionPopulation = new ArrayList<>();
Since we are also implementing search functionality, therefore, we need to replicate these variables to load new data on the page.
ArrayList<String> cn = new ArrayList<>();
List<String> cs = new ArrayList<>();
List<String> ac = new ArrayList<>();
List<String> tr = new ArrayList<>();
List<String> dt = new ArrayList<>();
List<String> nc = new ArrayList<>();
List<String> nd = new ArrayList<>();
List<String> sc = new ArrayList<>();
List<String> cpm = new ArrayList<>();
int searchedCountryPosition;
Now all the requisites have been taken care of so we can move on coding the real meat of the page.
Let's start by creating a bottom navigation bar. This bar is extremely important for any page as it helps users to navigate throughout the app. For the bottom navigation bar, we need to implement a nav listener which listens to the request from the user on each icon click and redirects the user to the appropriate page.
private BottomNavigationView.OnNavigationItemSelectedListener navListener = new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem)
{
switch (menuItem.getItemId()){
case R.id.nav_home:
Intent a = new Intent(CasesByCountry.this, Home.class);
startActivity(a);
break;
case R.id.nav_cases:
break;
case R.id.nav_world:
Intent b = new Intent(CasesByCountry.this, MapsActivity.class);
startActivity(b);
break;
case R.id.nav_news:
Intent c = new Intent(CasesByCountry.this, News.class);
startActivity(c);
break;
case R.id.nav_help:
Intent d = new Intent(CasesByCountry.this, Help.class);
startActivity(d);
break;
}
return false;
}
};
The onCreate method is the method that runs when the page is loaded so this is the real meat of the page.
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.cases_by_country);
Its time to initialize the variables that we created outside the method.
progressBar = findViewById(R.id.progressBar6);
btnRefresh = findViewById(R.id.btnRefresh2);
btnSearch = findViewById(R.id.btnSearch);
searchedText = (EditText) findViewById(R.id.txtSearch);
btnClear = findViewById(R.id.btnClear);
recyclerView = (RecyclerView) findViewById(R.id.recycler);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
listItems = new ArrayList<>();
listItems2 = new ArrayList<>();
All we did here is initialized the UI variables and list items that will be provided to the adapter.
We created a bottom navigation bar listener and its time to initialize it and set onNavigationListener to the navbar.
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
bottomNav.setOnNavigationItemSelectedListener(navListener);
Remember adapter requires the context of the page so while we are inside the onCreate method, go ahead and initialize the context by providing the context of the current page.
con = this;
Here comes the interesting operation part of the page. Since our page calls out to the REST API to get data, therefore, extracting the data process takes time. So we need to implement an asynchronous job in the main thread of the page so that the page doesn't show empty values. Since it takes time to extract therefore we need to tell the user that it will take some moment. So we need to implement a progress bar and remove the progress bar when the job is done and the page is ready to show the date.
So let's implement the progress bar and async task/job.
progressBar.setVisibility(View.VISIBLE);
new CasesByCountryAsync().execute();
We don't have CasesByCountryAsync() class so far. So let's develop it.
Note: If you don't know what is AsyncTask, then you refer to my previous blog in which I have briefly explained the beauty of asynchronous tasks.
Implement the async task class in the CasesByCountry class.
class CasesByCountryAsync extends AsyncTask {
We need to call the REST API using DataService class and provide the sorted to ListCasesDataProvider which later would be bound up using our very own ListCasesAdapter.
Let's start by initializing the list objects for sorted data fields.
@Override
protected Object doInBackground(Object[] objects) {
DataService ds = new DataService();
try
{
countryNames = ds.getCountryName();
cases = ds.getNumberOfCases();
deaths = ds.getNumberOfDeaths();
totalRecovered = ds.getTotalRecovered();
activeCases = ds.getActiveCases();
newCases = ds.getNewCases();
newDeaths = ds.getNewDeaths();
seriousCritical = ds.getSeriousCritical();
totalCasesPerMillionPopulation = ds.getTotalCasesPerMillionPopulation();
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
Now, this process will take of extracting data will about a second so we need to wait for a second to move on towards the next step. So let's go and implement wait() function.
int i =0;
synchronized (this)
{
while (i<10)
{
try {
wait(100);
i++;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
Here comes the part where we will tie up all the work we have done so far. On the UI thread, we will set extracted data inside data provider class. That data will be later provided to adapter to implement the recycler view functionality.
Implement the UI thread and extend it using a runnable method
runOnUiThread(new Runnable()
{
@Override
public void run()
{
Since we have different data associated with multiple countries, therefore, we need to use for loop to go through each country and instantiate the data provider class inside the loop.
try
{
for(int j = 0; j < countryNames.size(); j ++)
{
ListCasesDataProvider listCasesDataProvider = new ListCasesDataProvider("#" + (j+1) + " "+ countryNames.get(j),
cases.get(j), activeCases.get(j),
totalRecovered.get(j),
deaths.get(j), newCases.get(j), newDeaths.get(j), seriousCritical.get(j), totalCasesPerMillionPopulation.get(j));
listItems.add(listCasesDataProvider);
}
} catch(Exception e){
e.printStackTrace();
}
Instantiate the adapter class and set the adapter for recycler view
adapter = new ListCasesAdapter(listItems, con);
recyclerView.setAdapter(adapter);
}
return null;
}
});
That's it for doInBackground method. When all this process is done, we need to stop the progress bar which will reflect that job is successful.
@Override
protected void onPostExecute(Object o) {
progressBar.setVisibility(View.GONE);
}
Now we want to tell users that they can click each card on the list to acquire more statistics for any given country.
We can create a simple toast once the progress bar stops or the job is successfully done.
Toast toast = Toast.makeText(getApplicationContext(), "Click a country to see more stats", Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM, 0, 200);
toast.show();
Now pat yourself on your shoulder because you were able to implement the recycler view on the page. That's a great accomplishment. It took me a long time to figure all this out on my own.
The rest is easy.
Let's implement the refresh button functionality.
btnRefresh.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchedText.setBackgroundResource(0);
progressBar.setVisibility(View.VISIBLE);
new CasesByCountryAsync().execute();
}
});
The last component of this page is the search bar. It is quite an interesting part because of think about how would you let the user search for specific country cards among multiple countries from the recycler view?
I created an algorithm that lets you do that.
The user will enter the country name and the algorithm will find the country data from the data extracted from API. It will then generate the card for that specific country.
So, let's begin exploring the search functionality using the integrated setOnClickListener method.
btnSearch.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
closeKeyboard();
searchedText.setBackgroundResource(0);
I created this small function to close the keyboard so that the user doesn't have to close the keyboard manually as I realized that it can be tedius job.
private void closeKeyboard() {
View view = this.getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
searchedText.setBackgroundResource(0);
}
Back to the click listener.
try
{
progressBar.setVisibility(View.VISIBLE);
resultSet = filter(searchedText.getText().toString());
String searchedCountry = resultSet.get(0);
for (int i = 0; i < countryNames.size(); i++) {
if(searchedCountry == null)
{
Toast.makeText(getApplicationContext(), "Please enter a country", Toast.LENGTH_LONG).show();
}
else if (countryNames.get(i).matches(searchedCountry)) {
searchedCountryPosition = i;
}
}
DataService ds = new DataService();
cn = ds.getCountryName();
cs = ds.getNumberOfCases();
ac = ds.getActiveCases();
tr = ds.getTotalRecovered();
dt = ds.getNumberOfDeaths();
nc = ds.getNewCases();
nd = ds.getNewDeaths();
sc = ds.getSeriousCritical();
cpm = ds.getTotalCasesPerMillionPopulation();
}
The code mentioned above is the small algorithm that I am talking about. First it provides the search text to the function name filter() that spits out the list of data for that country. (Isn't that cool!?) Then the loop helps to determines the position of a country on the original country list. Once it knows what it is the position of a search country on the original list, it calls the data service to get the original list.
Before moving forward, I want to share the filter() method that I mentioned above.
private ArrayList<String> filter(String searchText) throws IOException, JSONException {
ArrayList<String> filteredList = new ArrayList<>();
DataService ds = new DataService();
countryNames = ds.getCountryName();
int i =0;
synchronized (this)
{
while (i<10)
{
try {
wait(100);
i++;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
for(String s : countryNames)
{
if(s.toLowerCase().matches(searchText.toLowerCase()))
{
filteredList.add(s);
}
}
return filteredList;
}
This method helps to retrieve the data related to the searched country. Note: Search country name is case sensitive.
Back to the click listener.
Now that we are calling to DataService to retrieve the original list we need to wait for about second to get all the data.
int i =0;
synchronized (this)
{
while (i<10)
{
try {
wait(100);
i++;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
We have the original list data. So now have to search the country data in the original list using the searched country position that we have retrieved earlier. Once we have searched country data, we can fill the recycler view card with the data using our custom adapter.
runOnUiThread(new Runnable()
{
@Override
public void run() {
try
{
ListCasesDataProvider listCasesDataProvider = new ListCasesDataProvider("#" + (searchedCountryPosition+1) + " "+ cn.get(searchedCountryPosition),
cs.get(searchedCountryPosition), ac.get(searchedCountryPosition),
tr.get(searchedCountryPosition),
dt.get(searchedCountryPosition), nc.get(searchedCountryPosition), nd.get(searchedCountryPosition), sc.get(searchedCountryPosition),cpm.get(searchedCountryPosition));
System.out.println(cn.get(searchedCountryPosition));
listItems2.removeAll(listItems2);
listItems2.add(listCasesDataProvider);
System.out.println(listItems2.toString());
} catch(Exception e){
e.printStackTrace();
}
adapter = new ListCasesAdapter(listItems2, con);
recyclerView.setAdapter(adapter);
}
});
progressBar.setVisibility(View.GONE);
}
And that's it my amigos! We have successfully implemented the search functionality. You might think that I am just keep dumping big chunks of code at you, but believe me that I have explained all of these things briefly in my previous blog of this series. So make sure to check that out as well.
There it is. The page that lists the valuable covid19 data of each country. It seems a lot but once you pick up the momentum, it gets easier.
You made it! 🎊
Conclusion
The statistics page is very important for the COVID-19 application because the user expects to see different countries in the app. This page helps to monitor the effect of COVID-19 throughout the world. There is a lot of work that goes into this page, but it is worth it because this page can help people to understand the global effect of coronavirus. Statistics are important when it comes to understanding why some countries are more affected by coronavirus than others. This page serves its true purpose that is to educate people about the seriousness of this pandemic. Implementing a ranking system on the page makes it easier to compare countries. There is a lot of thought that went through when building this page.
In the next blog, I will go through my favorite page and that is NEWS! How come someone calls this a one-stop application for COVID19 when the app doesn't have a news page from different countries. The news page will also use REST service (Google-News-API) to retrieve new of different countries from different credible sources. So stay tuned for that and we will meet in the next blog.
Stay home!
Stay Safe!
Top comments (0)