Fetch a list of popular TV shows using themoviedb API using Retrofit2 in Android

article

Retrofit is a type-safe HTTP client for Android and Java. It is mostly used to retrieve JSON data via a REST based webservices.It is developed by “Square”. Android Retrofit API is one of the most useful libraries which makes life of Android Developers easy to communicate with the server and get the data back from the server.

In this tutorial, we will be using Retrofit2 for performing network operations and parse JSON Data using Retrofit2 in Android and display in Recyclerview using cardview. The JSON Data contains information of the popular TV shows

Scope: The goal is to prepare the app that uses https://themoviedb.org API to fetch and display the most popular TV shows.

IDE: Android Studio.

API Endpoint: https://developers.themoviedb.org/3/tv/get-popular-tv-shows.

Procedure:

Step 1: Go to project level build.gradle file and add the following code into dependencies.

dependencies { compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' compile 'com.android.support:cardview-v7:25.3.1' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.github.bumptech.glide:glide:3.7.0' compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' }

This adds retrofit2, okhttp3 and glide for network and image fetching libraries in the project.

Step 2 : Go to Android Manifest file and add the following code.

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

This allow the app to have access to the Internet and detect the network state.

Step 3 : Create a Model Class Result.java. Below is the code for Model Class.

import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.io.Serializable; public class Result implements Serializable { @SerializedName("poster_path") @Expose private String posterPath; @SerializedName("overview") @Expose private String overview; @SerializedName("first_air_date") @Expose private String releaseDate; @SerializedName("original_name") @Expose private String originalTitle; @SerializedName("name") @Expose private String title; @SerializedName("backdrop_path") @Expose private String backdropPath; @SerializedName("vote_average") @Expose private Double voteAverage; public String getPosterPath() { return posterPath; } public void setPosterPath(String posterPath) { this.posterPath = posterPath; } public String getOverview() { return overview; } public void setOverview(String overview) { this.overview = overview; } public String getReleaseDate() { return releaseDate; } public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } public String getOriginalTitle() { return originalTitle; } public void setOriginalTitle(String originalTitle) { this.originalTitle = originalTitle; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBackdropPath() { return backdropPath; } public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } public Double getVoteAverage() { return voteAverage; } public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } }

Step 4: Wrap the List of data of Result.java Class with another model class.

import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; public class PopularTvShows { @SerializedName("page") @Expose private Integer page; @SerializedName("results") @Expose private List<Result> results = new ArrayList<Result>(); @SerializedName("total_results") @Expose private Integer totalResults; @SerializedName("total_pages") @Expose private Integer totalPages; public Integer getPage() { return page; } public void setPage(Integer page) { this.page = page; } public List<Result> getResults() { return results; } public void setResults(List<Result> results) { this.results = results; } public Integer getTotalResults() { return totalResults; } public void setTotalResults(Integer totalResults) { this.totalResults = totalResults; } public Integer getTotalPages() { return totalPages; } public void setTotalPages(Integer totalPages) { this.totalPages = totalPages; } }

Step 5: Create an Interface to fetch the JSON data using retrofit2. Below is the code for Interface:

import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface TvShowServices { @GET("tv/popular") Call<PopularTvShows> getPopularTvshows( @Query("api_key") String apiKey, @Query("language") String language, @Query("page") int pageIndex ); }

Step 6: Now create a TvshowApi.java class to fetch the popular tvshows and then use the class to display it in mainactivity of our app.

import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class TvShowApi { private static Retrofit retrofit = null; private static OkHttpClient buildClient() { return new OkHttpClient .Builder() .addInterceptor(new HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor .Level.BODY)) .build(); } public static Retrofit getClient() { if (retrofit == null) { retrofit = new Retrofit.Builder() .client(buildClient()) .addConverterFactory(GsonConverterFactory.create()) .baseUrl("https://api.themoviedb.org/3/") .build(); } return retrofit; } }

Step 7: Now create a recycler_row.xml layout for displaying them in the app in listview.

<?xml version="1.0" encoding="utf-8"?> <FrameLayout 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:orientation="vertical"> <android.support.v7.widget.CardView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/activity_margin_half" app:cardBackgroundColor="@color/placeholder_grey" app:cardCornerRadius="3dp" app:cardPreventCornerOverlap="false"> <ImageView android:id="@+id/tvshow_poster" android:layout_width="@dimen/poster_thumb_width" android:layout_height="@dimen/poster_thumb_height"/> </android.support.v7.widget.CardView> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="bottom" android:layout_marginTop="@dimen/activity_margin" android:background="@android:color/white" android:orientation="vertical" android:paddingBottom="@dimen/activity_margin" android:paddingEnd="@dimen/activity_margin" android:paddingLeft="@dimen/poster_thumb_width" android:paddingRight="@dimen/activity_margin" android:paddingStart="@dimen/poster_thumb_width" android:paddingTop="@dimen/activity_margin_half"> <TextView android:id="@+id/tvshow_title" style="@style/TextAppearance.AppCompat.Subhead" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/activity_margin_content" android:layout_marginStart="@dimen/activity_margin_content" android:layout_marginTop="@dimen/activity_margin_quarter" android:maxLines="1" /> <TextView android:id="@+id/tvshow_desc" style="@style/TextAppearance.AppCompat.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/activity_margin_content" android:layout_marginStart="@dimen/activity_margin_content" android:layout_marginTop="@dimen/activity_margin_half" android:ellipsize="end" android:maxLines="2" /> </LinearLayout> </FrameLayout>

Now we need to put them in a recyclerviewList. So now create activity_main.xml layout and write the below code.

<?xml version="1.0" encoding="utf-8"?> <FrameLayout 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" tools:context="com.saketkumar.swapcard.MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/main_recycler" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:paddingBottom="@dimen/activity_margin_content" android:paddingTop="@dimen/activity_margin_content"/> </FrameLayout>

Step 8: Now create a PaginationAdapter.Java class which extends RecyclerView to provide a bridge between data and displaying the view. Below is the code for this class:

import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; import com.bumptech.glide.request.target.Target; import java.util.ArrayList; import java.util.List; public class PaginationAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int ITEM = 0; private static final int LOADING = 1; private static final int HERO = 2; private static final String BASE_URL_IMG = "https://image.tmdb.org/t/p/w300"; private static final String BASE_URL_IMG_BACKGROUND = "https://image.tmdb.org/t/p/w780"; private ArrayList<Result> tvShowResults; private Context context; private boolean isLoadingAdded = false; private boolean retryPageLoad = false; private PaginationAdapterCallback mCallback; private String errorMsg; public PaginationAdapter(Context context) { this.context = context; this.mCallback = (PaginationAdapterCallback) context; tvShowResults = new ArrayList<>(); } public PaginationAdapter(ArrayList<Result> tvShowResults) { this.tvShowResults = tvShowResults; } public ArrayList<Result> getTvShows() { return tvShowResults; } public void setTvShows(ArrayList<Result> tvShowResults) { this.tvShowResults = tvShowResults; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder viewHolder = null; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch (viewType) { case ITEM: View viewItem = inflater.inflate(R.layout.item_list, parent, false); viewHolder = new TvShowsViewHolder(viewItem); break; case LOADING: View viewLoading = inflater.inflate(R.layout.item_progress, parent, false); viewHolder = new LoadingViewH(viewLoading); break; case HERO: View viewHero = inflater.inflate(R.layout.item_hero, parent, false); viewHolder = new HeaderViewH(viewHero); break; } return viewHolder; } @Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) { final Result result = tvShowResults.get(position); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Context context = view.getContext(); Intent intent = new Intent(context, DetailTvShowActivity.class); Bundle bundle = new Bundle(); bundle.putSerializable(Intent.EXTRA_TEXT, tvShowResults); bundle.putInt("POSITION", holder.getAdapterPosition()); intent.putExtras(bundle); context.startActivity(intent); } }); switch (getItemViewType(position)) { case HERO: final HeaderViewH heroVh = (HeaderViewH) holder; heroVh.mTvShowTitle.setText(result.getTitle()); heroVh.mTvShowDesc.setText(result.getOverview()); loadImage(BASE_URL_IMG_BACKGROUND + result.getBackdropPath()) .into(heroVh.mPosterImg) break; case ITEM: final TvShowsViewHolder tvShowsViewHolder = (TvShowsViewHolder) holder; tvShowsViewHolder.mTvShowTitle.setText(result.getTitle()); tvShowsViewHolder.mTvShowDesc.setText(result.getOverview()); GlideDrawableImageViewTarget imageViewPreview = new GlideDrawableImageViewTarget(tvShowsViewHolder.mPosterImg); loadImage(BASE_URL_IMG + result.getPosterPath()) .listener(new RequestListener<String, GlideDrawable>() { @Override public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) { tvShowsViewHolder.mProgress.setVisibility(View.GONE); return false; } @Override public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) { tvShowsViewHolder.mProgress.setVisibility(View.GONE); return false; } }) .into(imageViewPreview); break; case LOADING: LoadingViewH loadingVH = (LoadingViewH) holder; if (retryPageLoad) { loadingVH.mErrorLayout.setVisibility(View.VISIBLE); loadingVH.mProgressBar.setVisibility(View.GONE); loadingVH.mErrorTxt.setText( errorMsg != null ? errorMsg : context.getString(R.string.error_msg_unknown)); } else { loadingVH.mErrorLayout.setVisibility(View.GONE); loadingVH.mProgressBar.setVisibility(View.VISIBLE); } break; } } @Override public int getItemCount() { return tvShowResults == null ? 0 : tvShowResults.size(); } @Override public int getItemViewType(int position) { if (position == 0) { return HERO; } else { return (position == tvShowResults.size() - 1 && isLoadingAdded) ? LOADING : ITEM; } } private DrawableRequestBuilder<String> loadImage(@NonNull String posterPath) { return Glide .with(context) .load(posterPath) .diskCacheStrategy(DiskCacheStrategy.ALL) // cache both original & resized image .centerCrop() .crossFade(); } public void add(Result r) { tvShowResults.add(r); notifyItemInserted(tvShowResults.size() - 1); } public void addAll(List<Result> moveResults) { for (Result result : moveResults) { add(result); } } public void remove(Result r) { int position = tvShowResults.indexOf(r); if (position > -1) { tvShowResults.remove(position); notifyItemRemoved(position); } } public void clear() { isLoadingAdded = false; while (getItemCount() > 0) { remove(getItem(0)); } } public boolean isEmpty() { return getItemCount() == 0; } public void addLoadingFooter() { isLoadingAdded = true; add(new Result()); } public void removeLoadingFooter() { isLoadingAdded = false; int position = tvShowResults.size() - 1; Result result = getItem(position); if (result != null) { tvShowResults.remove(position); notifyItemRemoved(position); } } public Result getItem(int position) { return tvShowResults.get(position); } public void showRetry(boolean show, @Nullable String errorMsg) { retryPageLoad = show; notifyItemChanged(tvShowResults.size() - 1); if (errorMsg != null) this.errorMsg = errorMsg; } protected class HeaderViewH extends RecyclerView.ViewHolder { private TextView mTvShowTitle; private TextView mTvShowDesc; private ImageView mPosterImg; public HeaderViewH(View itemView) { super(itemView); mTvShowTitle = (TextView) itemView.findViewById(R.id.tvshow_title); mTvShowDesc = (TextView) itemView.findViewById(R.id.tvshow_desc); mPosterImg = (ImageView) itemView.findViewById(R.id.tvshow_poster); } } protected class TvShowsViewHolder extends RecyclerView.ViewHolder { private TextView mTvShowTitle; private TextView mTvShowDesc; private ImageView mPosterImg; private ProgressBar mProgress; public TvShowsViewHolder(View itemView) { super(itemView); mTvShowTitle = (TextView) itemView.findViewById(R.id.tvshow_title); mTvShowDesc = (TextView) itemView.findViewById(R.id.tvshow_desc); mPosterImg = (ImageView) itemView.findViewById(R.id.tvshow_poster); mProgress = (ProgressBar) itemView.findViewById(R.id.tvshow_progress); } } protected class LoadingViewH extends RecyclerView.ViewHolder implements View.OnClickListener { private ProgressBar mProgressBar; private ImageButton mRetryBtn; private TextView mErrorTxt; private LinearLayout mErrorLayout; public LoadingViewH(View itemView) { super(itemView); mProgressBar = (ProgressBar) itemView.findViewById(R.id.loadmore_progress); mRetryBtn = (ImageButton) itemView.findViewById(R.id.loadmore_retry); mErrorTxt = (TextView) itemView.findViewById(R.id.loadmore_errortxt); mErrorLayout = (LinearLayout) itemView.findViewById(R.id.loadmore_errorlayout); mRetryBtn.setOnClickListener(this); mErrorLayout.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.loadmore_retry: case R.id.loadmore_errorlayout: showRetry(false, null); mCallback.retryPageLoad(); break; } } } }

Step 9: Now in MainActivity.java class, we will load the json and display them in the list view. Below is the code for this class:

import android.content.Context; import android.net.ConnectivityManager; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import java.util.List; import java.util.concurrent.TimeoutException; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class MainActivity extends AppCompatActivity implements PaginationAdapterCallback { private static final String TAG = "MainActivity"; private static final int PAGE_START = 1; PaginationAdapter adapter; LinearLayoutManager linearLayoutManager; RecyclerView rv; ProgressBar progressBar; LinearLayout errorLayout; Button btnRetry; TextView txtError; private boolean isLoading = false; private boolean isLastPage = false; private int TOTAL_PAGES = 1002; private int currentPage = PAGE_START; private TvShowServices tvShowsService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); rv = (RecyclerView) findViewById(R.id.main_recycler); progressBar = (ProgressBar) findViewById(R.id.main_progress); errorLayout = (LinearLayout) findViewById(R.id.error_layout); btnRetry = (Button) findViewById(R.id.error_btn_retry); txtError = (TextView) findViewById(R.id.error_txt_cause); adapter = new PaginationAdapter(this); linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); rv.setLayoutManager(linearLayoutManager); rv.setItemAnimator(new DefaultItemAnimator()); rv.setAdapter(adapter); rv.addOnScrollListener(new PaginationScrollListener(linearLayoutManager) { @Override protected void loadMoreItems() { isLoading = true; currentPage += 1; loadNextPage(); } @Override public int getTotalPageCount() { return TOTAL_PAGES; } @Override public boolean isLastPage() { return isLastPage; } @Override public boolean isLoading() { return isLoading; } }); //init service and load data tvShowsService = TvShowApi.getClient().create(TvShowServices.class); loadFirstPage(); btnRetry.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { loadFirstPage(); } }); } private void loadFirstPage() { Log.d(TAG, "loadFirstPage: "); hideErrorView(); callPopulartvShowssApi().enqueue(new Callback<PopularTvShows>() { @Override public void onResponse(Call<PopularTvShows> call, Response<PopularTvShows> response) { hideErrorView(); List<Result> results = fetchResults(response); progressBar.setVisibility(View.GONE); adapter.addAll(results); if (currentPage <= TOTAL_PAGES) adapter.addLoadingFooter(); else isLastPage = true; } @Override public void onFailure(Call<PopularTvShows> call, Throwable t) { t.printStackTrace(); showErrorView(t); } }); } private List<Result> fetchResults(Response<PopularTvShows> response) { PopularTvShows PopulartvShowss = response.body(); return PopulartvShowss.getResults(); } private void loadNextPage() { Log.d(TAG, "loadNextPage: " + currentPage); callPopulartvShowssApi().enqueue(new Callback<PopularTvShows>() { @Override public void onResponse(Call<PopularTvShows> call, Response<PopularTvShows> response) { adapter.removeLoadingFooter(); isLoading = false; List<Result> results = fetchResults(response); adapter.addAll(results); if (currentPage != TOTAL_PAGES) adapter.addLoadingFooter(); else isLastPage = true; } @Override public void onFailure(Call<PopularTvShows> call, Throwable t) { t.printStackTrace(); adapter.showRetry(true, fetchErrorMessage(t)); } }); } private Call<PopularTvShows> callPopulartvShowssApi() { return tvShowsService.getPopularTvshows( getString(R.string.my_api_key), "en_US", currentPage ); } @Override public void retryPageLoad() { loadNextPage(); } private void showErrorView(Throwable throwable) { if (errorLayout.getVisibility() == View.GONE) { errorLayout.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); txtError.setText(fetchErrorMessage(throwable)); } } private String fetchErrorMessage(Throwable throwable) { String errorMsg = getResources().getString(R.string.error_msg_unknown); if (!isNetworkConnected()) { errorMsg = getResources().getString(R.string.error_msg_no_internet); } else if (throwable instanceof TimeoutException) { errorMsg = getResources().getString(R.string.error_msg_timeout); } return errorMsg; } private void hideErrorView() { if (errorLayout.getVisibility() == View.VISIBLE) { errorLayout.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); } } private boolean isNetworkConnected() { ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); return cm.getActiveNetworkInfo() != null; } }

Our Android Application Development Services, explains step by step programming method about "How to fetch a list of popular TV shows using themoviedb API using Retrofit2 in Android".

Final Result will be something like this:

article

Conclusion:

Run the application and you can see that all API data is displayed in Recyclerview with cardview using Retrofit2