Lesson Tuesday

You might have noticed that when we run the MyRestaurants app and change the orientation of the screen back and forth, our list activities restart each time; if we're looking at a restaurant's details, then rotate the device, it takes us back to the main list!

On an Android device, every time the orientation configuration changes the onDestroy() method is automatically called, thus restarting the current activity. Let's make sure our users can continue to view the same content seamlessly while transitioning between landscape and portrait orientations.

In this lesson we will create additional pathways of communication between our RestaurantListFragment and RestaurantListActivity by implementing a custom interface. This will allow our application to "remember" which restaurant we were viewing, and continue displaying the same content no matter how many times the user twists and turns their device.

Note: Because this process involves refactoring multiple areas throughout the application, you will likely receive errors until all steps depicted here are complete. This is completely normal.

Required Reading

If you haven't already in previous lessons, take time to read the Android documentation on Defining an Interface.

Defining Custom Interfaces

To start, let’s define an interface in the util sub-package called OnRestaurantSelectedListener, and place the following method inside:

OnRestaurantSelectedListener.java
public interface OnRestaurantSelectedListener {
    public void onRestaurantSelected(Integer position, ArrayList<Restaurant> restaurants);
}

Implementing Android Interfaces

We will use this interface to listen for the position of the currently-selected restaurant and the list of all restaurants from Yelp. Let's start by implementing our new interface and declaring our newmPosition and mRestaurants variables:

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity implements OnRestaurantSelectedListener {
    private Integer mPosition;
    ArrayList<Restaurant> mRestaurants;
    ...

Then, we'll override our interface's onRestaurantSelected() method. This method will be responsible for updating mRestaurants and mPosition with the list of all restaurants, and position in the ArrayListof the currently-selected restaurant.

RestaurantListActivity.java
...
    @Override
    public void onRestaurantSelected(Integer position, ArrayList<Restaurant> restaurants) {
        mPosition = position;
        mRestaurants = restaurants;
    }
...

Updating ViewHolders

Next, we need to update our RestaurantViewHolder to actively listen for the position of the currently-selected restaurant in the ArrayList of all restaurants. To do this, we'll add the OnRestaurantSelectedListener to our RestaurantListAdapter constructor, and pass it to any new ViewHolders we create:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    private static final int MAX_WIDTH = 200;
    private static final int MAX_HEIGHT = 200;

    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
    private Context mContext;
    private OnRestaurantSelectedListener mOnRestaurantSelectedListener;

    public RestaurantListAdapter(Context context, ArrayList<Restaurant> restaurants, OnRestaurantSelectedListener restaurantSelectedListener) {
        mContext = context;
        mRestaurants = restaurants;
        mOnRestaurantSelectedListener = restaurantSelectedListener;
    }

    @Override
    public RestaurantListAdapter.RestaurantViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.restaurant_list_item, parent, false);
        RestaurantViewHolder viewHolder = new RestaurantViewHolder(view, mRestaurants, mOnRestaurantSelectedListener);
        return viewHolder;
    }
    ...
}

Updating RestaurantListFragment

Next, we'll update our list fragment to pass the listener interface into the RestaurantListAdapter. In order to allow fragments and activities to communicate we need to capture an instance of our interface and cast it into the context of the activities we need to communicate with.

Thankfully, we can do this in something called the onAttach() method. Similar to the onCreate(), onCreateView(), and onDestroy() methods we've previously utilized, onAttach() is part of a fragment's built-in lifecycle and is always automatically called before onCreate(), as detailed in the Android Documentation on Fragment Lifecycles.

RestaurantListFragment.java
public class RestaurantListFragment extends Fragment {
    ...

    private OnRestaurantSelectedListener mOnRestaurantSelectedListener;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            mOnRestaurantSelectedListener = (OnRestaurantSelectedListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + e.getMessage());
        }
    }
    ...

    private void getRestaurants(String location) {
        ...
               getActivity().runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter = new RestaurantListAdapter(getActivity(), mRestaurants, mOnRestaurantSelectedListener);
                        mRecyclerView.setAdapter(mAdapter);
                        ...
                    }
                });
            }
        ...

Invoking Listeners

Next we need to invoke our interface method, onRestaurantSelected() when a specific restaurant is selected:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        ...
        private Context mContext;
        private int mOrientation;
        private ArrayList<Restaurant> mRestaurants = new ArrayList<>();
        private OnRestaurantSelectedListener mRestaurantSelectedListener;

        public RestaurantViewHolder(View itemView, ArrayList<Restaurant> restaurants, OnRestaurantSelectedListener restaurantSelectedListener) {
            super(itemView);
            ButterKnife.bind(this, itemView);

            mContext = itemView.getContext();
            mOrientation = itemView.getResources().getConfiguration().orientation;
            mRestaurants = restaurants;
            mRestaurantSelectedListener = restaurantSelectedListener;

            if (mOrientation == Configuration.ORIENTATION_LANDSCAPE){
                createDetailFragment(0);
            }
            itemView.setOnClickListener(this);
        }

    ...

    @Override
    public void onClick(View v) {
        int itemPosition = getLayoutPosition();
        mRestaurantSelectedListener.onRestaurantSelected(itemPosition, mRestaurants);
        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
            createDetailFragment(itemPosition); 
        } else {
        Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
        intent.putExtra(Constants.EXTRA_KEY_POSITION, itemPosition);
        intent.putExtra(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(mRestaurants));
        mContext.startActivity(intent);
         ...
  • First, we save the OnRestaurantClickListener that was passed into the view holder constructor as a member variable.

  • Then, we trigger our OnRestaurantSelectedListener interface by calling the onRestaurantSelected() method, passing the position and restaurants ArrayList.

These variables will now be available in any activities that implement this interface!

Updating Activities

Finally, we just need to tell our activities what to do with the data passed through our listener interface.

Before an activity is destroyed, the onSaveInstanceState() method is called. Inside of this method we want to save the existing position and restaurants ArrayList if they exist. Then, when the activity is recreated, we can pull out these variables in the savedInstanceState bundle that is passed in to our onCreate():

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity implements OnRestaurantSelectedListener {
    private Integer mPosition;
    ArrayList<Restaurant> mRestaurants;

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

        if (savedInstanceState != null) {

            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                mPosition = savedInstanceState.getInt(Constants.EXTRA_KEY_POSITION);
                mRestaurants = Parcels.unwrap(savedInstanceState.getParcelable(Constants.EXTRA_KEY_RESTAURANTS));

                if (mPosition != null && mRestaurants != null) {
                    Intent intent = new Intent(this, RestaurantDetailActivity.class);
                    intent.putExtra(Constants.EXTRA_KEY_POSITION, mPosition);
                    intent.putExtra(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(mRestaurants));
                    startActivity(intent);
                }

            }

        }

    }

     @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        if (mPosition != null && mRestaurants != null) {
            outState.putInt(Constants.EXTRA_KEY_POSITION, mPosition);
            outState.putParcelable(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(mRestaurants));
        }

    }

    @Override
    public void onRestaurantSelected(Integer position, ArrayList<Restaurant> restaurants) {
        mPosition = position;
        mRestaurants = restaurants;
    }
}

Now we just need to make sure that our RestaurantDetailActivity is pulling out the correct intent extras:

RestaurantDetailActivity.java
   ...

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

        mRestaurants = Parcels.unwrap(getIntent().getParcelableExtra(Constants.EXTRA_KEY_RESTAURANTS));
        int startingPosition =getIntent().getIntExtra(Constants.EXTRA_KEY_POSITION, 0);

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

We are now finished with the OnRestaurantSelectedListener implementation. Let’s run our app, rotate the device to activate landscape mode, Visit "Find Restaurants", select a restaurant from the list view...

detail-view-landscape

...And then rotate the device back to portrait mode. Our app should bring us to the (portrait) detail view for the restaurant that we selected!

detail-view-portrait


Example GitHub Repo for MyRestaurants

Examples


Additional Resources