Skip to main content

[Make-A] Movies app using TMDb API Part 4 – Networking using Retrofit Library

 

Retrofit is a type-safe HTTP client for Android and Java. I use this library a LOT be it at work or personal projects.

Here are 5 things why I like to use Retrofit:

  • It’s super easy to use.
  • Documentation is great and concise.
  • Easy to setup.
  • It makes me more productive and speeds up my development.
  • It solves the problem – an easy way to do networking in Android and Java

Table of Contents

  1. Part 1 – Specs and Introduction
  2. Part 2 – Requesting an API Key
  3. Part 3 – Movie List Item Layout Using ConstraintLayout
  4. Part 4 – Networking using Retrofit Library and Image Loading using Glide
  5. Part 5 – Pagination
  6. Part 6 – Sort Movies by Popular, Top Rated and Upcoming
  7. Part 7 – Using CoordinatorLayout, AppBarLayout, CollapsingToolbarLayout for Movie Details Screen
  8. Part 8 – Colors and a Free Launcher Icon Generator
  9. 5 Exercises and Thanks!

 

1. Importing Retrofit

Open your app-level build.gradle under app/ directory. Under dependencies import the library.

dependencies {
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
}

As of this writing, this is the latest version of Retrofit. Refer to Retrofit’s website for the latest version.

 

2. Importing Gson Converter

Gson is a Java serialization/deserialization library to convert Java Objects into JSON and back.

You might be wondering what’s the use of this?

Let’s say your backend is built with PHP. How does our Android app communicate with our backend? By calling APIs/endpoints.

Now, what’s their mode of communication? You’re backend can’t just send a PHP code to your Android app and expect to run it and vice versa right? They need some form of medium to communicate and that’s when JSON comes in. Treat JSON like the English language – the universal language.

After the data is converted to JSON. It’s up to the respective clients to serialize or deserialize.

serialize – converting an object to JSON

deserialize – converting a JSON to an object

Retrofit comes with Converters that does the job for of serialization/deserialization for us.

Open your app-level build.gradle under app/ directory. Import the converter

dependencies {
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
}

Refer to Retrofit’s website for the latest version.

 

3. Fetch movies from TMDb

1. Open your AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.imakeanapp.imapmovies">

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

    ....
</manifest>

2. Create a new class called Movie

public class Movie {

    @SerializedName("id")
    @Expose
    private int id;

    @SerializedName("title")
    @Expose
    private String title;

    @SerializedName("poster_path")
    @Expose
    private String posterPath;

    @SerializedName("release_date")
    @Expose
    private String releaseDate;

    @SerializedName("vote_average")
    @Expose
    private float rating;

    @SerializedName("genre_ids")
    @Expose
    private List<Integer> genreIds;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getPosterPath() {
        return posterPath;
    }

    public void setPosterPath(String posterPath) {
        this.posterPath = posterPath;
    }

    public String getReleaseDate() {
        return releaseDate;
    }

    public void setReleaseDate(String releaseDate) {
        this.releaseDate = releaseDate;
    }

    public float getRating() {
        return rating;
    }

    public void setRating(float rating) {
        this.rating = rating;
    }

    public List<Integer> getGenreIds() {
        return genreIds;
    }

    public void setGenreIds(List<Integer> genreIds) {
        this.genreIds = genreIds;
    }
}

3. Create a class called MoviesResponse

public class MoviesResponse {

    @SerializedName("page")
    @Expose
    private int page;

    @SerializedName("total_results")
    @Expose
    private int totalResults;

    @SerializedName("results")
    @Expose
    private List<Movie> movies;

    @SerializedName("total_pages")
    @Expose
    private int totalPages;

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getTotalResults() {
        return totalResults;
    }

    public void setTotalResults(int totalResults) {
        this.totalResults = totalResults;
    }

    public List<Movie> getMovies() {
        return movies;
    }

    public void setMovies(List<Movie> movies) {
        this.movies = movies;
    }

    public int getTotalPages() {
        return totalPages;
    }

    public void setTotalPages(int totalPages) {
        this.totalPages = totalPages;
    }
}

4. Create a new interface called OnGetMoviesCallback

public interface OnGetMoviesCallback {

    void onSuccess(List<Movie> movies);

    void onError();
}

5. Create a new interface called TMDbApi

public interface TMDbApi {

    @GET("movie/popular")
    Call<MoviesResponse> getPopularMovies(
            @Query("api_key") String apiKey,
            @Query("language") String language,
            @Query("page") int page
    );
}

That’s it! That’s all you need to be able to communicate with an API using Retrofit. Awesome isn’t it?! 😎

All we need is to call that function and pass the required parameters.

6. Create a new class called MoviesRepository

public class MoviesRepository {

    private static final String BASE_URL = "https://api.themoviedb.org/3/";
    private static final String LANGUAGE = "en-US";

    private static MoviesRepository repository;

    private TMDbApi api;

    private MoviesRepository(TMDbApi api) {
        this.api = api;
    }

    public static MoviesRepository getInstance() {
        if (repository == null) {
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

            repository = new MoviesRepository(retrofit.create(TMDbApi.class));
        }

        return repository;
    }

    public void getMovies(final OnGetMoviesCallback callback) {
        api.getPopularMovies("<YOUR_API_KEY>", LANGUAGE, 1)
                .enqueue(new Callback<MoviesResponse>() {
                    @Override
                    public void onResponse(@NonNull Call<MoviesResponse> call, @NonNull Response<MoviesResponse> response) {
                        if (response.isSuccessful()) {
                            MoviesResponse moviesResponse = response.body();
                            if (moviesResponse != null && moviesResponse.getMovies() != null) {
                                callback.onSuccess(moviesResponse.getMovies());
                            } else {
                                callback.onError();
                            }
                        } else {
                            callback.onError();
                        }
                    }

                    @Override
                    public void onFailure(Call<MoviesResponse> call, Throwable t) {
                        callback.onError();
                    }
                });
    }
}

What we did here is what you call a Singleton Pattern. We don’t want to have multiple instances of MoviesRepository where those instances do the same thing.

Instead, if we don’t have an instance of MoviesRepository yet, we create it. However, if there’s already an instance of MoviesRepository we just reuse that instance.

Only MoviesRepository will live in the app.

7. Open your MoviesAdapter.class

public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieViewHolder> {

    private List<Movie> movies;

    public MoviesAdapter(List<Movie> movies) {
        this.movies = movies;
    }

    @Override
    public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_movie, parent, false);
        return new MovieViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MovieViewHolder holder, int position) {
        holder.bind(movies.get(position));
    }

    @Override
    public int getItemCount() {
        return movies.size();
    }

    class MovieViewHolder extends RecyclerView.ViewHolder {
        TextView releaseDate;
        TextView title;
        TextView rating;
        TextView genres;

        public MovieViewHolder(View itemView) {
            super(itemView);
            releaseDate = itemView.findViewById(R.id.item_movie_release_date);
            title = itemView.findViewById(R.id.item_movie_title);
            rating = itemView.findViewById(R.id.item_movie_rating);
            genres = itemView.findViewById(R.id.item_movie_genre);
        }

        public void bind(Movie movie) {
            releaseDate.setText(movie.getReleaseDate().split("-")[0]);
            title.setText(movie.getTitle());
            rating.setText(String.valueOf(movie.getRating()));
            genres.setText("");
        }
    }
}

Let’s leave genres and and movie image. We will get to it later in this tutorial after we confirm that we are able to communicate with the API.

8. Open your MainActivity.class

public class MainActivity extends AppCompatActivity {

    private RecyclerView moviesList;
    private MoviesAdapter adapter;

    private MoviesRepository moviesRepository;

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

        moviesRepository = MoviesRepository.getInstance();

        moviesList = findViewById(R.id.movies_list);
        moviesList.setLayoutManager(new LinearLayoutManager(this));

        moviesRepository.getMovies(new OnGetMoviesCallback() {
            @Override
            public void onSuccess(List<Movie> movies) {
                adapter = new MoviesAdapter(movies);
                moviesList.setAdapter(adapter);
            }

            @Override
            public void onError() {
                Toast.makeText(MainActivity.this, "Please check your internet connection.", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

9. Run the app. And you should see something like this

We’re almost there! Next up the genres.

 

4. Fetch genres from TMDb

1. Create a class called Genre

public class Genre {

    @SerializedName("id")
    @Expose
    private int id;

    @SerializedName("name")
    @Expose
    private String name;

    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;
    }
}

2. Create a class called GenresResponse

public class GenresResponse {

    @SerializedName("genres")
    @Expose
    private List<Genre> genres;

    public List<Genre> getGenres() {
        return genres;
    }

    public void setGenres(List<Genre> genres) {
        this.genres = genres;
    }
}

3. Open your TMDbApi interface

public interface TMDbApi {

    ...

    @GET("genre/movie/list")
    Call<GenresResponse> getGenres(
            @Query("api_key") String apiKey,
            @Query("language") String language
    );
}

4. Open your MoviesAdapter.class

public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieViewHolder> {

    private List<Genre> allGenres;
    private List<Movie> movies;

    public MoviesAdapter(List<Movie> movies, List<Genre> allGenres) {
        this.movies = movies;
        this.allGenres = allGenres;
    }

    ...

    class MovieViewHolder extends RecyclerView.ViewHolder {
        ...

        public void bind(Movie movie) {
            releaseDate.setText(movie.getReleaseDate().split("-")[0]);
            title.setText(movie.getTitle());
            rating.setText(String.valueOf(movie.getRating()));
            genres.setText(getGenres(movie.getGenreIds()));
        }

        private String getGenres(List<Integer> genreIds) {
            List<String> movieGenres = new ArrayList<>();
            for (Integer genreId : genreIds) {
                for (Genre genre : allGenres) {
                    if (genre.getId() == genreId) {
                        movieGenres.add(genre.getName());
                        break;
                    }
                }
            }
            return TextUtils.join(", ", movieGenres);
        }
    }
}

5. Let’s refactor our MainActivity.class

public class MainActivity extends AppCompatActivity {

    ...

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

        moviesRepository = MoviesRepository.getInstance();

        moviesList = findViewById(R.id.movies_list);
        moviesList.setLayoutManager(new LinearLayoutManager(this));

        getGenres();
    }

    private void getGenres() {
        moviesRepository.getGenres(new OnGetGenresCallback() {
            @Override
            public void onSuccess(List<Genre> genres) {
                getMovies(genres);
            }

            @Override
            public void onError() {
                showError();
            }
        });
    }

    private void getMovies(final List<Genre> genres) {
        moviesRepository.getMovies(new OnGetMoviesCallback() {
            @Override
            public void onSuccess(List<Movie> movies) {
                adapter = new MoviesAdapter(movies, genres);
                moviesList.setAdapter(adapter);
            }

            @Override
            public void onError() {
                showError();
            }
        });
    }

    private void showError() {
        Toast.makeText(MainActivity.this, "Please check your internet connection.", Toast.LENGTH_SHORT).show();
    }
}

 

5. Loading our movie image using Glide

Glide is a fast and efficient image loading library for Android focused on smooth scrolling. At work, I use Picasso but for my personal projects and future projects I prefer this library. My overall experience with this library is great so far.

It’s just a matter of preference though.

1. Open your app-level build.gradle under app/ directory. Import the Glide library

dependencies {
    ...
    implementation ("com.github.bumptech.glide:glide:4.7.1") {
        exclude group: "com.android.support"
    }
    implementation "com.android.support:support-fragment:26.1.0"
}

Please refer to Glide’s website for the latest version.

2. Open your MoviesAdapter.class

public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieViewHolder> {
    private String IMAGE_BASE_URL = "http://image.tmdb.org/t/p/w500";

    ...

    class MovieViewHolder extends RecyclerView.ViewHolder {
        TextView releaseDate;
        TextView title;
        TextView rating;
        TextView genres;
        ImageView poster;

        public MovieViewHolder(View itemView) {
            super(itemView);
            releaseDate = itemView.findViewById(R.id.item_movie_release_date);
            title = itemView.findViewById(R.id.item_movie_title);
            rating = itemView.findViewById(R.id.item_movie_rating);
            genres = itemView.findViewById(R.id.item_movie_genre);
            poster = itemView.findViewById(R.id.item_movie_poster);
        }

        public void bind(Movie movie) {
            releaseDate.setText(movie.getReleaseDate().split("-")[0]);
            title.setText(movie.getTitle());
            rating.setText(String.valueOf(movie.getRating()));
            genres.setText(getGenres(movie.getGenreIds()));
            Glide.with(itemView)
                    .load(IMAGE_BASE_URL + movie.getPosterPath())
                    .apply(RequestOptions.placeholderOf(R.color.colorPrimary))
                    .into(poster);
        }

        ...
    }
}

 

6. Let’s test it out!

Run the app. You should see the images are now loading. And our app is slowly getting good.

Congratulations! 🎉 You’ve learned some useful library that will be handy to you in the future. Your toolbox has now grown. More will come from future tutorials in this blog. 😊

Our app is slowly getting better.

BUT! As you can see when we scroll down to the bottom, we ran out of movies!

It’s because TMDB API uses pagination when we fetch movies. Which is right. So that your api is lightweight and fast.

And besides, Android apps are memory intensive. You don’t to have thousands of movies all in one call.

In next tutorial we will handle Pagination.

Part 5 coming soon.