Flicks App Movie Details Activity Feature Guide
=====
This guide outlines the steps involved in implementing the Flicks App optional user story:
> When a movie is selected, expose details of a movie (ratings using RatingBar, popularity, and synopsis) in a separate activity
## Conceptual Overview
Assuming the basic functionality has been implemented, the list of movies from the [`/movie/now_playing` endpoint](https://developers.themoviedb.org/3/movies/get-now-playing) is being displayed from the primary activity as a list using `RecyclerView`, in which each row displays a `Movie` object created from the JSON data. To implement this feature, when the user selects a row in the list, a new activity should be displayed with additional details about the movie from the JSON data, including a RatingBar.
## Walkthrough
### 1. Create `MovieDetailsActivity`
Create a new Activity: via **File** ⇨ **New** ⇨ **Activity** ⇨ **Empty Activity**
- Name: `MovieDetailsActivity`
- Create it in the same package as the list activity
### 2. Integrate Parceler
First, [add Parceler to the `build.gradle` file](http://guides.codepath.com/android/Using-Parceler#setup) (be sure to sync)
Next, update the `Movie` class to be Parcelable:
1. Add the `@Parcel` annotation to class declaration
1. Make all fields **public** (remove any `private` qualifiers)
1. Add a default (no-arguments) constructor
```java
package com.codepath.flicks.model;
import org.json.JSONException;
import org.json.JSONObject;
import org.parceler.Parcel;
@Parcel // annotation indicates class is Parcelable
public class Movie {
// fields must be public for parceler
String title;
String overview;
String posterPath;
String backdropPath;
// no-arg, empty constructor required for Parceler
public Movie() {}
public Movie(JSONObject movie) throws JSONException {
title = movie.getString("title");
overview = movie.getString("overview");
posterPath = movie.getString("poster_path");
backdropPath = movie.getString("backdrop_path");
}
// ... additional code
}
```
### 3. Modify `MovieAdapter.ViewHolder`
> For more context, see [RecylerView - Simple Click Handler within ViewHolder](http://guides.codepath.com/android/Using-the-RecyclerView#simple-click-handler-within-viewholder)
Next, in `MovieAdapter`, find the nested `ViewHolder` class and modify it to display the new activity via an Intent when the user clicks on the row.
1. If the `ViewHolder` class is declared as a `static class`, _remove_ the `static` qualifier
2. Add implementation clause: `implements View.OnClickListener`
3. Override the `onClick(View)` method (right-click on class body, then **Generate...** ⇨ **Implement Methods...**)
4. In the `ViewHolder` constructor, call `itemView.setOnClickListener(this)`
5. Complete the `onClick(View)` implementation
- Get the position & ensure it's valid
- Get the `Movie` at that position in the list
- Create an `Intent` to display `MovieDetailsActivity`
- Pass the movie as an extra serialized via `Parcels.wrap()`
- Show the activity
```java
// class *cannot* be static
// implements View.OnClickListener
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
ImageView ivPrimaryImage;
TextView tvTitle;
TextView tvOverview;
public ViewHolder(View itemView) {
super(itemView);
ivPrimaryImage = (ImageView) itemView.findViewById(R.id.ivPrimaryImage);
tvTitle = (TextView) itemView.findViewById(R.id.tvTitle);
tvOverview = (TextView) itemView.findViewById(R.id.tvOverview);
// add this as the itemView's OnClickListener
itemView.setOnClickListener(this);
}
// when the user clicks on a row, show MovieDetailsActivity for the selected movie
@Override
public void onClick(View v) {
// gets item position
int position = getAdapterPosition();
// make sure the position is valid, i.e. actually exists in the view
if (position != RecyclerView.NO_POSITION) {
// get the movie at the position, this won't work if the class is static
Movie movie = movies.get(position);
// create intent for the new activity
Intent intent = new Intent(context, MovieDetailsActivity.class);
// serialize the movie using parceler, use its short name as a key
intent.putExtra(Movie.class.getSimpleName(), Parcels.wrap(movie));
// show the activity
context.startActivity(intent);
}
}
}
```
### 4. Modify `MovieDetailsActivity`
In `MovieDetailsActivity`, add some basic code to retrieve and unwrap the `Movie` from the `Intent`
1. Declare a new field for the movie
1. Retrieve, unwrap, and assign field from `onCreate`
1. Add some logging to confirm deserialization
```java
public class MovieDetailsActivity extends AppCompatActivity {
// the movie to display
Movie movie;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_movie_details);
// unwrap the movie passed in via intent, using its simple name as a key
movie = (Movie) Parcels.unwrap(getIntent().getParcelableExtra(Movie.class.getSimpleName()));
Log.d("MovieDetailsActivity", String.format("Showing details for '%s'", movie.getTitle()));
}
}
```
At this point, you should be able to test that you can click on a row and see the new activity. Check the logs via **Android Monitor** to confirm the correct `Movie` object is being passed.
### 5. Add `vote_average` to `Movie`
The [`/movie/now_playing` endpoint](https://developers.themoviedb.org/3/movies/get-now-playing) returns a `vote_average` value that represents the average ratings from all users on a scale from 0 to 10 inclusive. We'll display this value on the Movie Details Activity, but we need to addd it to the model first.
In the `Movie` class, add a new field to track the `vote_average` value returned from the API:
1. Declare the field: `Double voteAverage;`
1. Initialize it from JSON in constructor: `voteAverage = movie.getDouble("vote_average");`
1. Generate a getter for the value (right-click on class body, then **Generate...** ⇨ **Getter**)
### 5. Layout `activity_movie_details`
> For more context, see [Working with Input Views](https://guides.codepath.com/android/Working-with-Input-Views)
In `res/layout/activity_movie_details.xml`, layout the design to include the following elements:
1. A full-width `TextView` aligned to the top for the title
- ID: `tvTitle`
- textAppearance: `Large`
1. A `RatingBar` (under `Widgets`) aligned to the left under title
- ID: `rbVoteAverage`
- style: `Widget.AppCompat.RatingBar.Small`
- numStars: 5
- stepSize: 0.5
1. A full-width `TextView` aligned under the RatingBar for the overview/synopsis
- ID: `tvOverview`
The XML for your layout should look like this:
```xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.codepath.flicks.MovieDetailsActivity">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="Movie Title"
android:textAppearance="@android:style/TextAppearance.Large"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/tvOverview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:layout_below="@+id/rbVoteAverage"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<RatingBar
android:id="@+id/rbVoteAverage"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tvTitle"
android:numStars="5"
android:stepSize="0.5" />
</RelativeLayout>
```
### 6. Complete `MovieDetailsActivity` Wiring
1. Declare `TextView` fields for `tvTitle` and `tvOverview`
2. Declare `RatingBar` field for `rbVoteAverage`
3. Assign the fields in `onCreate()` via `findViewById`
4. Set the title and overview from the movie
5. Set the `RatingBar` value by diving `Movie.getVoteAverage` by `2.0`
```java
public class MovieDetailsActivity extends AppCompatActivity {
// the movie to display
Movie movie;
// the view objects
TextView tvTitle;
TextView tvOverview;
RatingBar rbVoteAverage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_movie_details);
// resolve the view objects
tvTitle = (TextView) findViewById(R.id.tvTitle);
tvOverview = (TextView) findViewById(R.id.tvOverview);
rbVoteAverage = (RatingBar) findViewById(R.id.rbVoteAverage);
// unwrap the movie passed in via intent, using its simple name as a key
movie = (Movie) Parcels.unwrap(getIntent().getParcelableExtra(Movie.class.getSimpleName()));
Log.d("MovieDetailsActivity", String.format("Showing details for '%s'", movie.getTitle()));
// set the title and overview
tvTitle.setText(movie.getTitle());
tvOverview.setText(movie.getOverview());
// vote average is 0..10, convert to 0..5 by dividing by 2
float voteAverage = movie.getVoteAverage().floatValue();
rbVoteAverage.setRating(voteAverage / 2.0f);
}
}
```
### 7. Make It Your Own
The above is just a basic design; you should customize it and make it your own. Refer to the [`/movie/now_playing` endpoint docs](https://developers.themoviedb.org/3/movies/get-now-playing) to see what other values or available...or go the extra mile and pull in data from another endpoint.
### References
- [Using Intents to Create Flows](https://guides.codepath.com/android/Using-Intents-to-Create-Flows)
- [Passing Complex Data in a Bundle](https://guides.codepath.com/android/Using-Intents-to-Create-Flows#passing-complex-data-in-a-bundle)
- [Using Parceler](http://guides.codepath.com/android/Using-Parceler)
- [Using Parcelable](http://guides.codepath.com/android/Using-Parcelable)
- [Working with Input Views](https://guides.codepath.com/android/Working-with-Input-Views)
- [RecylerView - Simple Click Handler within ViewHolder](http://guides.codepath.com/android/Using-the-RecyclerView#simple-click-handler-within-viewholder)