Android Retrofit - JSON GridView Images and Text


Android Retrofit JSON GridView Example and Tutorial.

In this tutorial we want to see how to download JSON data from online, then parse that data using Google GSON and then bind the data to a custom GridView. The data will comprise images and text and we render them in custom cardviews. We use Retrofit, a modern and fast http client for android.

Let's start.

Video Tutorial

Demo

Here is the demo of this project.

Android Retrofit GridView JSON

Here's it in the landscape mode:

Android Retrofit GridView JSON

Build.gradle

This is where we add our dependencies. These dependencies in our case include:

  1. AppCompat - a support library giving us access to the AppCompatActivity which we'll use as our MainActivity's base class.
  2. ConstraintLayout - Our constraint layout.
  3. CardView - to give us the CardView class which will be the root element of our GridView.
  4. Retrofit - our http client.
  5. Gson Converter - To map our JSON data to java classes.
  6. Picasso - To load our images from online.

You add these in your app level build.gradle file.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation "com.android.support:cardview-v7:28.0.0"
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
}

MainActivity.java

Retrofit Classes We use

We will start by adding several import statements. These are for the several API's we'll use. And these include first the Retrofit2 classes like the:

  1. Call - This is an interface that represents a HTTP call we make. Or a HTTP Request. In this case we will be making a HTTP GET request.
  2. Callback - Represents the Callback that we receive. This can be a success callback or a failure callback. If it's a success callback then we get a response. Then we can parse that response or map to a list of our data objects. Then that list can then be bound to the gridview.
  3. Response - Response the response we receive. That response will have our data.
Widget and View Related Classes We use

Not only will we use retrofit classes but we'll also have widgets and views. This is because the JSON data we download has to be rendered to the user. We use a widgets to successful render that complex data. Here are some classes we need.

  1. BaseAdapter - Our json data we download is a complex data. Even after parsing it using the GsonConverterFactory we can't just render it without adapting it to our views. So we need a custom BaseAdapter class to allow that adaptation.
  2. LayoutInflater - We have to create a custom gridview to handle our data. In fact not a custom gridview but gridview with custom view items. It means the gridview will have custom views. To create that custom views we need a custom layout. However for that layout to be converted to a View object it has to undergo a process called Layout Inflation. Basically parsing an xml layout into a view object. That needs the LayoutInflater class.
  3. ProgressBar - To show progress of our download as we download data asynchronously.
  4. ImageView - To render images from the web.
  5. TextView - To render texts.
  6. Toast - To show short messages when a given cardview in our gridview is clicked.
ImageLoading Classes

We will also be able to load our images from json. For that we can use one of the most popular imageloading libraries out there, Picasso, named after spanish Painter Pablo Picasso.

Marking Class Fields with Attributes

Firs we will be creating a data object class. This is a class representing our data object, in this case a Spacecraft. So basically our JSON data is a JSONArray comprising Spacecraft JSONObjects. We will be using Gson library to map our java data object to our json objects.

For that we need to flower our instance fields with attributes starting with @SerializedName(), where we pass the key of the JSON Object fields in that attribute.

    class Spacecraft {
        /*
        INSTANCE FIELDS
         */
        @SerializedName("id")
        private int id;
        @SerializedName("name")
        private String name;
        @SerializedName("propellant")
        private String propellant;
        @SerializedName("imageurl")
        private String imageURL;
        @SerializedName("technologyexists")
        private int technologyExists;

        .....
Creating a Retrofit API interface

Retrofit is a type safe HTTP Client that's unique in that it allows you to represent your APIs using interface. For example in our project we are downloading json data from online. So we will be making a HTTP GET request. We can use represent that with an interface which then we can work with throughout our code.

Here's the interface we use in this example.


    interface MyAPIService {

        @GET("/Oclemy/SampleJSON/338d9585/spacecrafts.json")
        Call<List<Spacecraft>> getSpacecrafts();
    }
Creating a Retrofit Factory class

That is a class responsible for hosting our factory or generator method. That is a method responsible for giving us one instance of Retrofit we can always be using. This prevents us from polluting our code with multiple instances of Retrofit which will make us make HTTP requests we don't intend.

Here's the class:

    static class RetrofitClientInstance {

        private static Retrofit retrofit;
        private static final String BASE_URL = "https://raw.githubusercontent.com/";

        public static Retrofit getRetrofitInstance() {
            if (retrofit == null) {
                retrofit = new Retrofit.Builder()
                        .baseUrl(BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build();
            }
            return retrofit;
        }
    }

You can see we've also supplied the Base url. Then we've checked if retrofit is null before obtaining it's instance.

Downloading JSON via Retrofit

This means making our HTTP GET call. But first we have to create our API service

        MyAPIService myAPIService = RetrofitClientInstance.getRetrofitInstance().create(MyAPIService.class);

Then reference our Call object. We said the Call is an interface representing a HTTP Call. As a generic type we pass a List of spacecrafts. Then we invoke the method that will actually make the call.

        Call<List<Spacecraft>> call = myAPIService.getSpacecrafts();

To actually make the call we have to invoke either execute() or enqueue(). We use the latter as it allows us make an asynchronous call, a call that is executed in the background thread. Thus we are able to not freeze our user interface.

Here's how we make the call and handle the response.

        call.enqueue(new Callback<List<Spacecraft>>() {

            @Override
            public void onResponse(Call<List<Spacecraft>> call, Response<List<Spacecraft>> response) {
                myProgressBar.setVisibility(View.GONE);
                populateGridView(response.body());
            }
            @Override
            public void onFailure(Call<List<Spacecraft>> call, Throwable throwable) {
                myProgressBar.setVisibility(View.GONE);
                Toast.makeText(MainActivity.this, throwable.getMessage(), Toast.LENGTH_LONG).show();
            }
        });

Okay here's the complete code:

package info.camposha.retrofitgridviewimages;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.GridView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.google.gson.annotations.SerializedName;
import com.squareup.picasso.Picasso;

import java.util.List;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;

public class MainActivity extends AppCompatActivity {

    class Spacecraft {
        /*
        INSTANCE FIELDS
         */
        @SerializedName("id")
        private int id;
        @SerializedName("name")
        private String name;
        @SerializedName("propellant")
        private String propellant;
        @SerializedName("imageurl")
        private String imageURL;
        @SerializedName("technologyexists")
        private int technologyExists;

        public Spacecraft(int id, String name, String propellant, String imageURL, int technologyExists) {
            this.id = id;
            this.name = name;
            this.propellant = propellant;
            this.imageURL = imageURL;
            this.technologyExists = technologyExists;
        }

        /*
         *GETTERS AND SETTERS
         */
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getPropellant() {
            return propellant;
        }

        public String getImageURL() {
            return imageURL;
        }

        public int getTechnologyExists() {
            return technologyExists;
        }

        /*
        TOSTRING
         */
        @Override
        public String toString() {
            return name;
        }
    }

    interface MyAPIService {

        @GET("/Oclemy/SampleJSON/338d9585/spacecrafts.json")
        Call<List<Spacecraft>> getSpacecrafts();
    }

    static class RetrofitClientInstance {

        private static Retrofit retrofit;
        private static final String BASE_URL = "https://raw.githubusercontent.com/";

        public static Retrofit getRetrofitInstance() {
            if (retrofit == null) {
                retrofit = new Retrofit.Builder()
                        .baseUrl(BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build();
            }
            return retrofit;
        }
    }

    class GridViewAdapter extends BaseAdapter{

        private List<Spacecraft> spacecrafts;
        private Context context;

        public GridViewAdapter(Context context,List<Spacecraft> spacecrafts){
            this.context = context;
            this.spacecrafts = spacecrafts;
        }

        @Override
        public int getCount() {
            return spacecrafts.size();
        }

        @Override
        public Object getItem(int pos) {
            return spacecrafts.get(pos);
        }

        @Override
        public long getItemId(int pos) {
            return pos;
        }

        @Override
        public View getView(int position, View view, ViewGroup viewGroup) {
            if(view==null)
            {
                view=LayoutInflater.from(context).inflate(R.layout.model,viewGroup,false);
            }

            TextView nameTxt = view.findViewById(R.id.nameTextView);
            TextView txtPropellant = view.findViewById(R.id.propellantTextView);
            CheckBox chkTechExists = view.findViewById(R.id.myCheckBox);
            ImageView spacecraftImageView = view.findViewById(R.id.spacecraftImageView);

            final Spacecraft thisSpacecraft= spacecrafts.get(position);

            nameTxt.setText(thisSpacecraft.getName());
            txtPropellant.setText(thisSpacecraft.getPropellant());
            chkTechExists.setChecked( thisSpacecraft.getTechnologyExists()==1);
            chkTechExists.setEnabled(false);

            if(thisSpacecraft.getImageURL() != null && thisSpacecraft.getImageURL().length()>0)
            {
                Picasso.get().load(thisSpacecraft.getImageURL()).placeholder(R.drawable.placeholder).into(spacecraftImageView);
            }else {
                Toast.makeText(context, "Empty Image URL", Toast.LENGTH_LONG).show();
                Picasso.get().load(R.drawable.placeholder).into(spacecraftImageView);
            }

            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(context, thisSpacecraft.getName(), Toast.LENGTH_SHORT).show();
                }
            });

            return view;
        }
    }

    private GridViewAdapter adapter;
    private GridView mGridView;
    ProgressBar myProgressBar;

    private void populateGridView(List<Spacecraft> spacecraftList) {
        mGridView = findViewById(R.id.mGridView);
        adapter = new GridViewAdapter(this,spacecraftList);
        mGridView.setAdapter(adapter);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ProgressBar myProgressBar= findViewById(R.id.myProgressBar);
        myProgressBar.setIndeterminate(true);
        myProgressBar.setVisibility(View.VISIBLE);

        /*Create handle for the RetrofitInstance interface*/
        MyAPIService myAPIService = RetrofitClientInstance.getRetrofitInstance().create(MyAPIService.class);

        Call<List<Spacecraft>> call = myAPIService.getSpacecrafts();
        call.enqueue(new Callback<List<Spacecraft>>() {

            @Override
            public void onResponse(Call<List<Spacecraft>> call, Response<List<Spacecraft>> response) {
                myProgressBar.setVisibility(View.GONE);
                populateGridView(response.body());
            }
            @Override
            public void onFailure(Call<List<Spacecraft>> call, Throwable throwable) {
                myProgressBar.setVisibility(View.GONE);
                Toast.makeText(MainActivity.this, throwable.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
    }
}
//end

activity_main.xml

This is the layout that will contain our main activity's layout. At the root we will be having a LinearLayout, a class that arranges our items linearly. Inside it we have a TextView, a class that allows us render texts. Then we also have a Progressbar. The latter will allow us show progress as we download data in the background thread.

Finally we have a GridView, an adapterview that allows us render data in grids.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/headerTxt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Retrofit GridView JSON"
        android:padding="5dp"
        android:textAlignment="center"
        android:textStyle="bold"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textColor="@color/colorAccent" />

    <ProgressBar
        android:id="@+id/myProgressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:indeterminateBehavior="cycle"
        android:visibility="gone" />

    <GridView
        android:id="@+id/mGridView"
        android:layout_weight="0.5"
        android:numColumns="auto_fit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

model.xml

This layout will be inflated into a single item view for our GridView. At the root we have a CardView. This then allows our gridview to comprise of awesome cards. Inside the CardView we will have an imageview to render images. Also we have textviews and checkbox.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_margin="5dp"
    card_view:cardCornerRadius="10dp"
    card_view:cardElevation="5dp"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/spacecraftImageView"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:padding="10dp"
            android:src="@drawable/placeholder"  />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Name"
            android:id="@+id/nameTextView"
            android:padding="5dp"
            android:textColor="@color/colorAccent"
            android:layout_below="@+id/spacecraftImageView"
            android:layout_alignParentLeft="true"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text=" Propellant"
            android:textStyle="italic"
            android:id="@+id/propellantTextView"
            android:padding="5dp"
            android:layout_below="@+id/nameTextView"
            android:layout_alignParentLeft="true"
            />

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/myCheckBox"
            android:text="Tech Exists?"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true" />

    </RelativeLayout>
</android.support.v7.widget.CardView>

AndroidManifest.xml

In your android manifest you have to add permission for internet connectivity. Read more about permissions here. If you don't add this permission then you application won't be able to access the internet.

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

How do You Feel after reading this?

According to scientists, we humans have 8 primary innate emotions: joy, acceptance, fear, surprise, sadness, disgust, anger, and anticipation. Feel free to tell us how you feel about this article using these emotes or via the comment section. This feedback helps us gauge our progress.

Help me Grow.

I set myself some growth ambitions I desire to achieve by this year's end regarding this website and my youtube channel. Am halfway. Help me reach them by:




Recommendations


What do You Think


Previous Post Next Post