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)