Lesson Monday

We've just created a RestaurantDetailFragment and corresponding layout to display details of each individual restaurant. Additionally, we've integrated a RecyclerView to continually recycle these fragments for optimal performance. However, the pieces of information displayed in our fragments are just placeholders. That's not very exciting. Let's change it.

In this lesson we'll learn how to communicate between Android components, specifically providing data to fragments using something called parcelables. To do this, we'll learn a little more about serialization, implement a tool called Parceler into our project, and create a special PagerAdapter to act as a bridge between our data and fragment.

Overview

To pass information between different components of an Android application, the information must be serialized. Serialization is the conversion of an object into bytes, so the object can be easily saved somewhere, or efficiently passed to another area of an application. Conversely, deserialization is the act of using those bytes to re-create that object or data.

What?

OK, let's unpack this a little more. Imagine passing the String "A herd of cats" from one activity to another. No big deal - a String is not a very complex datatype. It has an easily calculable length, and a fairly limited amount of methods. As a result, it's fairly lightweight, and it's pretty easy for our app to know how much memory and CPU time might be necessary to make that transaction.

But what if we are not transferring a String? What if we were transferring a giant ArrayList of Cat objects from one spot to another instead? An ArrayList's size is mutable, so we must compute that first. And a Cat object could hold a lot of properties - name, food, age, and so on. Hmm. This is getting bulky.

...And what if we were actually moving an ArrayList of Animaltypes around? Each Animal object could contain sub-ArrayLists of Cats, Dogs, Bats and more; each with their own properties and ArrayLists... you get the idea. This could get very large, very quickly!

Understanding how much memory to reserve and pulling CPU power from the main UI thread to complete this transaction...that could really slow things down. But if we serialize our Object into one String of data on the outset we know exactly how many bytes of data we need, and aren't required to calculate that information on the fly. Serialization also allows us to snapshot data at a specific moment and store it, instead of rebuilding individual objects later.

serialization_sm.png

Android contains an interface called Parcelable to manage the process of serialization. We could implement this interface and override its methods manually, but it's widely considered easier to use a library called Parceler to manage this process for us.

Parceler is a third-party library that generates all the boilerplate source code for the Android Parcelable interface, thereby helping us manage the process of serializing data, and passing it to our fragments to make passing things around smooth and efficient. Make sure you can tell an interviewer the difference between Serializable and Parcelable.

Required Reading: Parceler Library and Configuration

We're almost ready to allow our individual components to begin communicating with one another. To do this, we'll add the Parceler library in our MyRestaurants application. Parceler will handle implementing the functionality of the Parcelable Android interface for us.

Before we begin, Follow along with this tutorial to configure MyRestaurants to use Parceler: Using Parceler.

Implementing Parceler

Do not move forward until you've followed along with the Using Parceler tutorial linked above. You will need to update both your project and app build.gradle files and add the @Parcel annotation to our Restaurant class. You will also need to add an empty default Restaurant constructor.

Adding Annotations

Once complete, your Restaurant.java class file should look like this:

Restaurant.java
package com.epicodus.myrestaurants.models;

import org.parceler.Parcel;

import java.util.ArrayList;

@Parcel
public class Restaurant {
    String name;
    String phone;
    String website;
    double rating;
    String imageUrl;
    ArrayList<String> address = new ArrayList<>();
    double latitude;
    double longitude;
    ArrayList<String> categories = new ArrayList<>();

    public Restaurant() {}

    public Restaurant(String name, String phone, String website,
                      double rating, String imageUrl, ArrayList<String> address,
                      double latitude, double longitude, ArrayList<String> categories) {

        this.name = name;
        this.phone = phone;
        this.website = website;
        this.rating = rating;
        this.imageUrl = imageUrl;
        this.address = address;
        this.latitude = latitude;
        this.longitude = longitude;
        this.categories = categories;
    }

    public String getName() {
        return name;
    }

    public String getPhone() {
        return phone;
    }

    public String getWebsite() {
        return  website;
    }

    public double getRating() {
        return rating;
    }

    public String getImageUrl(){
        return imageUrl;
    }

    public ArrayList<String> getAddress() {
        return address;
    }

    public double getLatitude() {
        return latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public ArrayList<String> getCategories() {
        return categories;
    }
}

Bundling Information with Parceler

After setting up Parceler, we can move our focus to the RestaurantDetailFragment. Add the following code to RestaurantDetailFragment. Afterwards we will walk through what each method does:

RestaurantDetailFragment.java
package com.epicodus.myrestaurants.ui;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.epicodus.myrestaurants.R;
import com.epicodus.myrestaurants.models.Restaurant;
import com.squareup.picasso.Picasso;

import org.parceler.Parcels;

import butterknife.Bind;
import butterknife.ButterKnife;

public class RestaurantDetailFragment extends Fragment {
    @Bind(R.id.restaurantImageView) ImageView mImageLabel;
    @Bind(R.id.restaurantNameTextView) TextView mNameLabel;
    @Bind(R.id.cuisineTextView) TextView mCategoriesLabel;
    @Bind(R.id.ratingTextView) TextView mRatingLabel;
    @Bind(R.id.websiteTextView) TextView mWebsiteLabel;
    @Bind(R.id.phoneTextView) TextView mPhoneLabel;
    @Bind(R.id.addressTextView) TextView mAddressLabel;
    @Bind(R.id.saveRestaurantButton) TextView mSaveRestaurantButton;

    private Restaurant mRestaurant;

    public static RestaurantDetailFragment newInstance(Restaurant restaurant) {
        RestaurantDetailFragment restaurantDetailFragment = new RestaurantDetailFragment();
        Bundle args = new Bundle();
        args.putParcelable("restaurant", Parcels.wrap(restaurant));
        restaurantDetailFragment.setArguments(args);
        return restaurantDetailFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mRestaurant = Parcels.unwrap(getArguments().getParcelable("restaurant"));
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_restaurant_detail, container, false);
        ButterKnife.bind(this, view);

        Picasso.with(view.getContext()).load(mRestaurant.getImageUrl()).into(mImageLabel);

        mNameLabel.setText(mRestaurant.getName());
        mCategoriesLabel.setText(android.text.TextUtils.join(", ", mRestaurant.getCategories()));
        mRatingLabel.setText(Double.toString(mRestaurant.getRating()) + "/5");
        mPhoneLabel.setText(mRestaurant.getPhone());
        mAddressLabel.setText(android.text.TextUtils.join(", ", mRestaurant.getAddress()));

        return view;
    }
}
  • The first method, newInstance(), is used instead of a constructor and returns a new instance of our RestaurantDetailFragment. We use the Parceler library to add our restaurant object to our bundle and set the bundle as the argument for our new RestaurantDetailFragment. This allows us to access necessary data when a new instance of our fragment is created.

  • The next method, onCreate(), is called when the fragment is created. Here, we unwrap our restaurant object from the arguments we added in the newInstance() method.

  • In onCreateView(), this restaurant object is then used to set our ImageView and TextViews.

For now, we are not going to set the text for the website. Instead, we will use the website string URL when we learn more about implicit intents in an upcoming lesson.

PagerAdapter

Now let’s create the adapter which will connect our data to the fragment view. Inside of our adapters sub-package, create a new class called RestaurantPagerAdapter:

adapters/RestaurantPagerAdapter

package com.epicodus.myrestaurants.adapters;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

import com.epicodus.myrestaurants.models.Restaurant;
import com.epicodus.myrestaurants.ui.RestaurantDetailFragment;

import java.util.ArrayList;

public class RestaurantPagerAdapter extends FragmentPagerAdapter {
    private ArrayList<Restaurant> mRestaurants;

    public RestaurantPagerAdapter(FragmentManager fm, ArrayList<Restaurant> restaurants) {
        super(fm);
        mRestaurants = restaurants;
    }

    @Override
    public Fragment getItem(int position) {
        return RestaurantDetailFragment.newInstance(mRestaurants.get(position));
    }

    @Override
    public int getCount() {
        return mRestaurants.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mRestaurants.get(position).getName();
    }
}

Let’s review what each of these methods does.

  • RestaurantPagerAdapter() is a constructor where we set the required FragmentManager and array list of restaurants we will be swiping through.

  • The next method, getItem() returns an instance of the RestaurantDetailFragment for the restaurant in the position provided as an argument.

  • getCount() simply determines how many restaurants are in our Array List. This lets our adapter know how many fragments it must create.

  • getPageTitle() updates the title that appears in the scrolling tabs at the top of the screen.

ViewHolder ClickListener

Now that our fragment is set up, we want to be able to click on a restaurant in our RecyclerView and navigate to that individual restaurant's detail page, which should be populated with information about that specific restaurant. To achieve this, let’s revisit our RestaurantListAdapter and add a click listener using the View.OnClickListener interface:

RestaurantListAdapter.java
...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
       ...
        public RestaurantViewHolder(View itemView) {
            ...
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            int itemPosition = getLayoutPosition();
            Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
            intent.putExtra("position", itemPosition);
            intent.putExtra("restaurants", Parcels.wrap(mRestaurants));
            mContext.startActivity(intent);
        }
    ...

Here, we instruct the RestaurantViewHolder class to implement the View.OnClickListener interface. Then, we set our listener in the RestaurantViewHolder constructor.

When the ItemView is clicked, the onClick() method will execute. It uses getLayoutPosition() to retrieve the position of the specific list item clicked. Then, it creates an intent to navigate to our RestaurantDetailActivity, with the itemPosition and the ArrayList of restaurants included as intent extras.

To include an entire ArrayList as an intent extra, we use the Parcels.wrap() method. This handles the process of serializing the data using Android's Parcelable interface.

Set PagerAdapter

Now we can add our new RestaurantPagerAdapter to our RestaurantDetailActivity:

RestaurantDetailActivity.java
package com.epicodus.myrestaurants.ui;

import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;

import com.epicodus.myrestaurants.R;
import com.epicodus.myrestaurants.adapters.RestaurantPagerAdapter;
import com.epicodus.myrestaurants.models.Restaurant;

import org.parceler.Parcels;

import java.util.ArrayList;

import butterknife.Bind;
import butterknife.ButterKnife;


public class RestaurantDetailActivity extends AppCompatActivity {
    @Bind(R.id.viewPager) ViewPager mViewPager;
    private RestaurantPagerAdapter adapterViewPager;
    ArrayList<Restaurant> mRestaurants = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_restaurant_detail);
        ButterKnife.bind(this);

        mRestaurants = Parcels.unwrap(getIntent().getParcelableExtra("restaurants"));
        int startingPosition = getIntent().getIntExtra("position", 0);

        adapterViewPager = new RestaurantPagerAdapter(getSupportFragmentManager(), mRestaurants);
        mViewPager.setAdapter(adapterViewPager);
        mViewPager.setCurrentItem(startingPosition);
    }
}

Inside of the onCreate() method, we pull out our ArrayList<Restaurant> Parcelable using the unwrap() method on our "restaurants" intent extra. We also retrieve the position int included as an intent extra.

We create a new pager adapter called adapterViewPager, providing the mRestaurants as an argument. Then, we instruct our ViewPager to use this new adapter. We also set the current item to the position of the item that was just clicked on.

And voilà, now we have a functioning fragment pager adapter!

Further Exploration: Parcelables

If you'd like to explore the Parcelable interface more in-depth, check out Code Path's article Using Parcelable.


Example GitHub Repo for MyRestaurants

Terminology


  • Serialization: The conversion of an object into bytes, so the object can be easily saved somewhere, or efficiently passed to another area of an application.

  • Parcelable: An Android object used to pass data between different components of an application. It does this by serializing Java Objects between contexts.

  • Parceler: A commonly-used Android library to manage the process of Parcelables.

Examples


Additional Resources


  • The Parceler Website includes a great overview and detailed documentation.

  • The Code Path tutorial Using Parceler details setting up Parceler in an Android application.

  • If you'd like to explore the Parcelable interface more in-depth, check out Code Path's article Using Parcelable.