Rotten Tomatoes Networking Tutorial - meswapnilwagh/android_guides GitHub Wiki
This is a tutorial about building a basic app for displaying popular movies currently in theaters using the unauthenticated RottenTomatoes API.
Checkout out this slide deck for a basic walkthrough.
Let's build an Android app step by step. The app will display a list of popular movies currently in theaters which includes the poster, title, and cast of each film.
The app will do the following:
- Fetch the box office movies from the Box Office Movie API in JSON format
- Deserialize the JSON data for each of the movies into
BoxOfficeMovie
objects - Build an array of
BoxOfficeMovie
objects and create anArrayAdapter
for those movies - Define
getView
to define how to inflate a layout for each movie row and display each movie's data. - Attach the adapter for the movies to a ListView to display the data on screen
To do this, we can organize the different components into the following objects:
-
RottenTomatoesClient
- Responsible for executing the API requests and retrieving the JSON -
BoxOfficeMovie
- Model object responsible for encapsulating the attributes for each individual movie -
BoxOfficeMoviesAdapter
- Responsible for mapping eachBoxOfficeMovie
to a particular view layout -
BoxOfficeActivity
- Responsible for fetching and deserializing the data and configuring the adapter
The full source code for this app can be downloaded here for review as well.
RottenTomatoes has an extensive API which includes a Movie Search, Lists, Box Office Movies and much more. We can use one of these APIs to create a simple demonstration of networking with Android.
First, we need to ensure we have an API Key which is the mechanism used to authorize our API requests. We cannot make any API calls without first registering for an account (or signing in) and then setting up an API key.
Hint: If you don't have a key, feel free to use the key 9htuhtcb4ymusd73d4z6jxcj
as you follow the demo below.
In particular, this app will use the Box Office Movies endpoint to retrieve a list of movies currently playing in theaters. The url to retrieve this data is http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?apikey=<api-key-here>
. The response is as follows:
{
"movies": [{
"id": "770687943",
"title": "Harry Potter and the Deathly Hallows - Part 2",
"year": 2011,
"mpaa_rating": "PG-13",
"runtime": 130,
"critics_consensus": "Thrilling, powerfully acted, and visually dazzling...",
"release_dates": {"theater": "2011-07-15"},
"ratings": {
"critics_rating": "Certified Fresh",
"critics_score": 97,
"audience_rating": "Upright",
"audience_score": 93
},
"synopsis": "Harry Potter and the Deathly Hallows, is the final adventure...",
"posters": {
"thumbnail": "http://content8.flixster.com/movie/11/15/86/11158674_mob.jpg",
"profile": "http://content8.flixster.com/movie/11/15/86/11158674_pro.jpg",
"detailed": "http://content8.flixster.com/movie/11/15/86/11158674_det.jpg",
"original": "http://content8.flixster.com/movie/11/15/86/11158674_ori.jpg"
},
"abridged_cast": [
{
"name": "Daniel Radcliffe",
"characters": ["Harry Potter"]
},
{
"name": "Rupert Grint",
"characters": [
"Ron Weasley",
"Ron Wesley"
]
}
]
},
{
"id": "770687943",
...
}]
}
As you can see the API returns a dictionary with a single "movies" key which contains an array of json dictionaries which each represent the data for a particular film. This is the response we will parse and use to create our application.
Before we continue, we need to setup the android-async-http-client for sending asynchronous network requests.
Let's also install a library for remote image loading called Picasso so we can easily display movie posters.
Edit the app/build.gradle
file and add them as dependencies that can be downloaded via a remote repository:
repositories {
jcenter()
}
dependencies {
// ...
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.loopj.android:android-async-http:1.4.6'
}
Then you need to sync with gradle and make sure there are no errors.
Make sure to download the two placeholder movie posters:
Copy and paste both small_movie_poster.png and large_movie_poster.png into res/drawable
for use later. You can drag and drop these files into the appropriate folders assuming you have selected the Project
dropdown inside Android Studio.
Add the proper internet permissions to the AndroidManifest.xml
within the manifest
node:
<uses-permission android:name="android.permission.INTERNET" />
Once you add this, network requests can be executed. Make sure that the permissions are defined before the <application>...</application> is declared. Otherwise, you may notice that your network requests are quietly being ignored.
Generate a project with the name "RottenTomatoesDemo", a minimum API of 10 and a first activity called BoxOfficeActivity. In the res/layout/activity_box_office.xml
, let's drop out a ListView with the id lvMovies
:
<RelativeLayout 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=".BoxOfficeActivity" >
<ListView
android:id="@+id/lvMovies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true" >
</ListView>
</RelativeLayout>
Let's generate a Java class that will act as our RottenTomato API client for sending out the network requests to the specific endpoints. Create a Java class called RottenTomatoesClient
with the following code:
public class RottenTomatoesClient {
private final String API_KEY = "...getkey...";
private final String API_BASE_URL = "http://api.rottentomatoes.com/api/public/v1.0/";
private AsyncHttpClient client;
public RottenTomatoesClient() {
this.client = new AsyncHttpClient();
}
private String getApiUrl(String relativeUrl) {
return API_BASE_URL + relativeUrl;
}
}
Next, let's define a method for accessing the Box Office Movies API within our client by executing an asynchronous request to the appropriate endpoint:
public class RottenTomatoesClient {
// ...
// http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?apikey=<key>
public void getBoxOfficeMovies(JsonHttpResponseHandler handler) {
String url = getApiUrl("lists/movies/box_office.json");
RequestParams params = new RequestParams("apikey", API_KEY);
client.get(url, params, handler);
}
// ...
}
Looking at the JSON response format for the box office movies API, we can understand what information we will be provided:
{
"movies": [{
"id": "770687943",
"title": "Harry Potter and the Deathly Hallows - Part 2",
"year": 2011,
"mpaa_rating": "PG-13",
"runtime": 130,
"critics_consensus": "Thrilling, powerfully acted, and visually dazzling...",
"release_dates": {"theater": "2011-07-15"},
"ratings": {
"critics_rating": "Certified Fresh",
"critics_score": 97,
"audience_rating": "Upright",
"audience_score": 93
},
"synopsis": "Harry Potter and the Deathly Hallows, is the final adventure...",
"posters": {
"thumbnail": "http://content8.flixster.com/movie/11/15/86/11158674_mob.jpg",
"profile": "http://content8.flixster.com/movie/11/15/86/11158674_pro.jpg",
"detailed": "http://content8.flixster.com/movie/11/15/86/11158674_det.jpg",
"original": "http://content8.flixster.com/movie/11/15/86/11158674_ori.jpg"
},
"abridged_cast": [
{
"name": "Daniel Radcliffe",
"characters": ["Harry Potter"]
},
{
"name": "Rupert Grint",
"characters": [
"Ron Weasley",
"Ron Wesley"
]
}
]
},
{
"id": "770687943",
...
}]
}
Next, let's define a model that represents the relevant data for a single box office movie. This movie needs to contain basic info for each movie
such as title
, year
, synopsis
, posters.thumbnail
, ratings.critics_score
and the abridged_cast
. Create a Java class called BoxOfficeMovie
with the relevant attributes defined:
public class BoxOfficeMovie {
private String title;
private int year;
private String synopsis;
private String posterUrl;
private int criticsScore;
private ArrayList<String> castList;
public String getTitle() {
return title;
}
public int getYear() {
return year;
}
public String getSynopsis() {
return synopsis;
}
public String getPosterUrl() {
return posterUrl;
}
public int getCriticsScore() {
return criticsScore;
}
public String getCastList() {
return TextUtils.join(", ", castList);
}
}
Next, let's add a factory method for deserializing the JSON response in order to construct an instance of the BoxOfficeMovie
class:
public class BoxOfficeMovie {
// ...
// Returns a BoxOfficeMovie given the expected JSON
// BoxOfficeMovie.fromJson(movieJsonDictionary)
// Stores the `title`, `year`, `synopsis`, `poster` and `criticsScore`
public static BoxOfficeMovie fromJson(JSONObject jsonObject) {
BoxOfficeMovie b = new BoxOfficeMovie();
try {
// Deserialize json into object fields
b.title = jsonObject.getString("title");
b.year = jsonObject.getInt("year");
b.synopsis = jsonObject.getString("synopsis");
b.posterUrl = jsonObject.getJSONObject("posters").getString("thumbnail");
b.criticsScore = jsonObject.getJSONObject("ratings").getInt("critics_score");
// Construct simple array of cast names
b.castList = new ArrayList<String>();
JSONArray abridgedCast = jsonObject.getJSONArray("abridged_cast");
for (int i = 0; i < abridgedCast.length(); i++) {
b.castList.add(abridgedCast.getJSONObject(i).getString("name"));
}
} catch (JSONException e) {
e.printStackTrace();
return null;
}
// Return new object
return b;
}
// ...
}
Let's also add a convenience static method for parsing an array of JSON movie dictionaries into an ArrayList<BoxOfficeMovie>
which we will use to manage the box office movie API endpoint response:
public class BoxOfficeMovie {
// ...
// Decodes array of box office movie json results into movie model objects
// BoxOfficeMovie.fromJson(jsonArrayOfMovies)
public static ArrayList<BoxOfficeMovie> fromJson(JSONArray jsonArray) {
ArrayList<BoxOfficeMovie> movies = new ArrayList<BoxOfficeMovie>(jsonArray.length());
// Process each result in json array, decode and convert to movie
// object
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject moviesJson = null;
try {
moviesJson = jsonArray.getJSONObject(i);
} catch (Exception e) {
e.printStackTrace();
continue;
}
BoxOfficeMovie movie = BoxOfficeMovie.fromJson(moviesJson);
if (movie != null) {
movies.add(movie);
}
}
return movies;
}
// ...
}
Now, we have the API Client (RottenTomatoesClient
) and the data Model object (BoxOfficeModel
). Final step is to construct an adapter that allows us to translate a BoxOfficeMovie
into a particular view. Let's generate a Java class called BoxOfficeMoviesAdapter
which extends from ArrayAdapter<BoxOfficeMovie>
:
public class BoxOfficeMoviesAdapter extends ArrayAdapter<BoxOfficeMovie> {
public BoxOfficeMoviesAdapter(Context context, ArrayList<BoxOfficeMovie> aMovies) {
super(context, 0, aMovies);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO: Complete the definition of the view for each movie
return null;
}
}
Now, we need to define a layout to use for visualizing a particular movie. Let's create a layout file called item_box_office_movie.xml
with a RelativeLayout
root layout for defining the movie row layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp" >
<ImageView
android:id="@+id/ivPosterImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxHeight="70dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="@drawable/small_movie_poster" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_toRightOf="@+id/ivPosterImage"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textStyle="bold"
android:hint="The Dark Knight"
android:textSize="12sp" />
<TextView
android:id="@+id/tvCriticsScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tvTitle"
android:layout_below="@+id/tvTitle"
android:hint="91%"
android:textSize="12sp" />
<TextView
android:id="@+id/tvCast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tvCriticsScore"
android:maxLines="1"
android:ellipsize="end"
android:hint="Christian Bale, Joseph-Gordon Levitt, Heath Ledger, Maggie Gylenhall"
android:textSize="12sp" />
</RelativeLayout>
</RelativeLayout>
and now we can fill in the getView
method to finish off the BoxOfficeMoviesAdapter
:
public class BoxOfficeMoviesAdapter extends ArrayAdapter<BoxOfficeMovie> {
// ...
// Translates a particular `BoxOfficeMovie` given a position
// into a relevant row within an AdapterView
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
BoxOfficeMovie movie = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.item_box_office_movie, parent, false);
}
// Lookup views within item layout
TextView tvTitle = (TextView) convertView.findViewById(R.id.tvTitle);
TextView tvCriticsScore = (TextView) convertView.findViewById(R.id.tvCriticsScore);
TextView tvCast = (TextView) convertView.findViewById(R.id.tvCast);
ImageView ivPosterImage = (ImageView) convertView.findViewById(R.id.ivPosterImage);
// Populate the data into the template view using the data object
tvTitle.setText(movie.getTitle());
tvCriticsScore.setText("Score: " + movie.getCriticsScore() + "%");
tvCast.setText(movie.getCastList());
Picasso.with(getContext()).load(movie.getPosterUrl()).into(ivPosterImage);
// Return the completed view to render on screen
return convertView;
}
// ...
}
Now that we have defined the adapter, let's plug all of these components together within the BoxOfficeActivity
by using the client to send out an API call, deserialize the response into an array of BoxOfficeMovie
object and then render those into a ListView using the BoxOfficeMoviesAdapter
. Let's start by initializing the ArrayList, adapter, and the ListView:
public class BoxOfficeActivity extends Activity {
private ListView lvMovies;
private BoxOfficeMoviesAdapter adapterMovies;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_box_office);
lvMovies = (ListView) findViewById(R.id.lvMovies);
ArrayList<BoxOfficeMovie> aMovies = new ArrayList<BoxOfficeMovie>();
adapterMovies = new BoxOfficeMoviesAdapter(this, aMovies);
lvMovies.setAdapter(adapterMovies);
}
}
Now let's make the call to the box office API from the activity using our client:
public class BoxOfficeActivity extends Activity {
RottenTomatoesClient client;
protected void onCreate(Bundle savedInstanceState) {
// ...
// Fetch the data remotely
fetchBoxOfficeMovies();
}
// Executes an API call to the box office endpoint, parses the results
// Converts them into an array of movie objects and adds them to the adapter
private void fetchBoxOfficeMovies() {
client = new RottenTomatoesClient();
client.getBoxOfficeMovies(new JsonHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, JSONObject responseBody) {
JSONArray items = null;
try {
// Get the movies json array
items = responseBody.getJSONArray("movies");
// Parse json array into array of model objects
ArrayList<BoxOfficeMovie> movies = BoxOfficeMovie.fromJson(items);
// Load model objects into the adapter
for (BoxOfficeMovie movie : movies) {
adapterMovies.add(movie); // add movie through the adapter
}
adapterMovies.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
}
After running the app, we should see the populated box office movie data:
The full source code for this app can be downloaded here for review as well.
Suppose we wanted to add a detail view which would display more details about a movie when the movie was selected within the list. In order to achieve that we would need to:
- Generate an activity for the details view
- Define the detail Layout XML with views
- Setup an item click listener for the list of movies
- Fire an intent when a user selects a movie from the list
- Pass the movie through the intent and populate the detail view
First, let's generate the activity that will display all the details by selecting File => New => Other => Android Activity
and naming our activity "BoxOfficeDetailActivity". This activity should simply display additional information about the film for people to view, let's edit the layout res/layout/activity_box_office_detail.xml
to include all the movie details and a larger poster:
<RelativeLayout 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:padding="5dp"
tools:context=".BoxOfficeDetailActivity" >
<!-- @drawable/large_movie_poster sourced from
http://content8.flixster.com/movie/11/15/86/11158674_pro.jpg -->
<ImageView
android:id="@+id/ivPosterImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:maxHeight="160dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/ivPosterImage"
android:textStyle="bold"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@+id/ivPosterImage"
android:text="The Dark Knight"
android:textSize="18sp" />
<TextView
android:id="@+id/tvCriticsScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tvTitle"
android:layout_below="@+id/tvTitle"
android:layout_marginTop="5dp"
android:text="Critics Score: 93%"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAudienceScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tvCriticsScore"
android:layout_below="@+id/tvCriticsScore"
android:layout_marginTop="5dp"
android:text="Audience Score: 93%"
android:textSize="14sp" />
<TextView
android:id="@+id/tvCast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tvAudienceScore"
android:layout_below="@+id/tvAudienceScore"
android:layout_marginTop="5dp"
android:text="Christian Bale, Joseph-Gordon Levitt"
android:textSize="12sp" />
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/ivPosterImage"
android:layout_margin="10dp" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/tvCriticsConsensus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is an excellent movie that has action and romance" />
<TextView
android:id="@+id/tvSynopsis"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="This is a story about a protagonist defeating an antagonist" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
Next, we want to be able to pass the movie object from the list to this detail view, so let's make our BoxOfficeMovie
serializable:
public class BoxOfficeMovie implements Serializable {
private static final long serialVersionUID = -8959832007991513854L;
// ...
}
We also want to be able to access a few key attributes of the API data for the detail page such as a larger picture and critics consensus:
public class BoxOfficeMovie implements Serializable {
// ...
private String largePosterUrl;
private String criticsConsensus;
private int audienceScore;
public static BoxOfficeMovie fromJson(JSONObject jsonObject) {
// ...
b.largePosterUrl = jsonObject.getJSONObject("posters").getString("detailed");
b.criticsConsensus = jsonObject.getString("critics_consensus");
b.audienceScore = jsonObject.getJSONObject("ratings").getInt("audience_score");
// ...
}
// ...
public String getLargePosterUrl() {
return largePosterUrl;
}
public String getCriticsConsensus() {
return criticsConsensus;
}
public int getAudienceScore() {
return audienceScore;
}
}
Let's now add an item click handler to our BoxOfficeActivity
which will launch the detail view with an intent:
public class BoxOfficeActivity extends Activity {
// ...
public static final String MOVIE_DETAIL_KEY = "movie";
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
setupMovieSelectedListener();
}
// ...
public void setupMovieSelectedListener() {
lvMovies.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View item, int position, long rowId) {
// Launch the detail view passing movie as an extra
Intent i = new Intent(BoxOfficeActivity.this, BoxOfficeDetailActivity.class);
i.putExtra(MOVIE_DETAIL_KEY, adapterMovies.getItem(position));
startActivity(i);
}
});
}
}
Now, let's populate the data from the movie into the details view with BoxOfficeDetailActivity
:
public class BoxOfficeDetailActivity extends Activity {
private ImageView ivPosterImage;
private TextView tvTitle;
private TextView tvSynopsis;
private TextView tvCast;
private TextView tvAudienceScore;
private TextView tvCriticsScore;
private TextView tvCriticsConsensus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_box_office_detail);
// Fetch views
ivPosterImage = (ImageView) findViewById(R.id.ivPosterImage);
tvTitle = (TextView) findViewById(R.id.tvTitle);
tvSynopsis = (TextView) findViewById(R.id.tvSynopsis);
tvCast = (TextView) findViewById(R.id.tvCast);
tvCriticsConsensus = (TextView) findViewById(R.id.tvCriticsConsensus);
tvAudienceScore = (TextView) findViewById(R.id.tvAudienceScore);
tvCriticsScore = (TextView) findViewById(R.id.tvCriticsScore);
// Use the movie to populate the data into our views
BoxOfficeMovie movie = (BoxOfficeMovie)
getIntent().getSerializableExtra(BoxOfficeActivity.MOVIE_DETAIL_KEY);
loadMovie(movie);
}
// Populate the data for the movie
public void loadMovie(BoxOfficeMovie movie) {
// Populate data
tvTitle.setText(movie.getTitle());
tvCriticsScore.setText(Html.fromHtml("<b>Critics Score:</b> " + movie.getCriticsScore() + "%"));
tvAudienceScore.setText(Html.fromHtml("<b>Audience Score:</b> " + movie.getAudienceScore() + "%"));
tvCast.setText(movie.getCastList());
tvSynopsis.setText(Html.fromHtml("<b>Synopsis:</b> " + movie.getSynopsis()));
tvCriticsConsensus.setText(Html.fromHtml("<b>Consensus:</b> " + movie.getCriticsConsensus()));
// R.drawable.large_movie_poster from
// http://content8.flixster.com/movie/11/15/86/11158674_pro.jpg -->
Picasso.with(this).load(movie.getLargePosterUrl()).
placeholder(R.drawable.large_movie_poster).
into(ivPosterImage);
}
}
With this, we should now be able to view the movie detail view:
The full source code for this app can be downloaded here for review as well.