Lesson Wednesday

As our last week working through our ongoing MyRestaurants application draws to a close, let's continue to put a few more finishing touches in place. Currently, if users navigate to their list of saved restaurants and view a restaurant's details they still see the "Save Restaurant" button; even though they've obviously already saved this restaurant!

This occurs in both portrait and landscape layouts:

portrait-detail-view

landscape-detail-view

However, we still want to keep our project streamlined and well-refactored by re-using the same RestaurantDetailFragment in both SavedRestaurantListActivity and RestaurantListActivity. After all, this button should be the only difference between the two.

In this lesson we'll learn how to hide and show specific elements of a fragment based on additional context about the fragment's usage; such as where the user is navigating to it from. In our application specifically, we will hide the "Save Restaurant" button on the RestaurantDetailFragment if the user accesses it through the SavedRestaurantListActivity.

Determining Location

First, we need to determine the area the user is accessing the RestaurantDetailFragment through, and make this information accessible to the fragment itself. We'll do this by including additional information with the OnRestaurantSelectedListener we created in the last lesson.

We'll structure this information in the form of an additional argument to its singular onRestaurantSelected() method. As you know, intent extras are key-value pairs. To minimize room for error we'll place one key and two potential values in our Constants class:

Constants.java
...
    public static final String KEY_SOURCE = "source";
    public static final String SOURCE_SAVED = "saved";
    public static final String SOURCE_FIND = "find";
...

Here, KEY_SOURCE will represent the key in the key-value pair of the intent extra. SOURCE_SAVED will be the corresponding value when the user navigates to this fragment through the SavedRestaurantListActivity, and SOURCE_FIND will represent the value when the user travels from the RestaurantListActivity (or, the "Find Restaurants" area of our application).

Passing Location Information Through Listeners

Now, we'll use the key and value options we've placed in our Constants class to include this additional information when creating a new instance of our OnRestaurantSelected listener interface.

First, we'll add an additional source parameter to its singular onRestaurantSelected() method:

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

source will be the String name of the activity the user views our reusable fragment from; Either "RestaurantListActivity" or "SavedRestaurantListActivity", in our case.

We're currently implementing this interface in our RestaurantListActivity. Let's make sure to also include source information anywhere we're already dealing with the mRestaurants and mPosition data our listener is already handling:

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity implements OnRestaurantSelectedListener {
     ...
    String mSource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        if (savedInstanceState != null) {

            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                ...
                mSource = savedInstanceState.getString(Constants.KEY_SOURCE);

                if (mPosition != null && mRestaurants != null) {
                    ...
                    intent.putExtra(Constants.KEY_SOURCE, mSource);
                    startActivity(intent);
                }
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        ...
        if (mPosition != null && mRestaurants != null) {
            ...
            outState.putString(Constants.KEY_SOURCE, mSource);
        }
    }

    @Override
    public void onRestaurantSelected(Integer position, ArrayList<Restaurant> restaurants, String source) {
        ...
        mSource = source;
    }
}

As we covered in the last lesson, our OnRestaurantClickListener is being passed into our RestaurantViewHolder as a member variable, then triggered in its onRestaurantSelected() method in onClick(), where it's passed information about which restaurant was clicked. Let's also pass information regarding the user's current location (either SavedRestaurantListActivity or RestaurantListActivity) here too:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        ...
         @Override
           public void onClick(View v) {
                int itemPosition = getLayoutPosition();
                mRestaurantSelectedListener.onRestaurantSelected(itemPosition, mRestaurants, Constants.SOURCE_FIND);
                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));
                    intent.putExtra(Constants.KEY_SOURCE, Constants.SOURCE_FIND);
                    mContext.startActivity(intent);
                }
            }
      ...

Here, we include our SOURCE_FIND key if the user is viewing our restaurant's details through the RestaurantListActivity (ie: "Find Restaurants" area of our application). This will eventually let our fragment know that we should display the "Save Restaurant" button in this area, because these are not the user's saved restaurants, and they should therefore have the option to save any.

Let's make sure we also include this information if the device is in landscape orientation, too. Because the conditional above simply calls createDetailFragment() directly when the phone is in landscape, we'll need to add the additional information describing where the user came from in this method:'

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        ...
    private void createDetailFragment(int position){
        RestaurantDetailFragment detailFragment = RestaurantDetailFragment.newInstance(mRestaurants, position, Constants.SOURCE_FIND);
        FragmentTransaction ft = ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.restaurantDetailContainer, detailFragment);
        ft.commit();
    }
...

Here, we've included the additional parameter Constants.SOURCE_FIND when we call RestaurantDetailFragment's newInstance() method. This will pass along a string referring to the source location that we placed in our Constants class. In this case, "find". Note: You will likely receive an error regarding this extra argument; but don't worry, we'll address this in just a moment.

FirebaseRestaurantListAdapter

Next, we'll need to follow a similar process in the onClick() override of our FirebaseRestaurantListAdapter. Here, we'll include intent extra information indicating that the user is viewing our restaurant's details from the SavedRestaurantListActivity. Because these restaurants are already saved to the user's list, we'll remove the "Save Restaurant" button.

FirebaseRestauarantListAdapter.java
    ...
        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                int itemPosition = viewHolder.getAdapterPosition();
                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));
                    intent.putExtra(Constants.KEY_SOURCE, Constants.SOURCE_SAVED);
                    mContext.startActivity(intent);
                }
            }
        });
    ...

Let's make sure we also include this information if the device is in landscape orientation in the Saved Restaurants area, too. Similar to what we just did in the RestaurantListAdapter, we'll need to add the additional information describing where the user came from in this method:

FirebaseRestaurantListAdapter.java
...
    private void createDetailFragment(int position){
        RestaurantDetailFragment detailFragment = RestaurantDetailFragment.newInstance(mRestaurants, position, Constants.SOURCE_SAVED);
        FragmentTransaction ft = ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.restaurantDetailContainer, detailFragment);
        ft.commit();
    }
...

Here, we've included the additional parameter Constants.SOURCE_SAVED. Again, this will pass along a string from our Constants class referring to the source (in this case "saved", because our source is the SavedRestaurantListActivity).

Retrieving Source Information

Let's make use of this information we've included once we reach RestaurantDetailActivity. First, we'll gather the source information we packaged up in the intent extra. We'll assign this value to the member variable mSource.

Let's declare it in RestaurantDetailActivity. Then, we'll define it by fetching the extra information we included with our intent:

RestaurantDetailActivity.java
public class RestaurantDetailActivity extends AppCompatActivity {
    private String mSource;
    ...
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mSource = getIntent().getStringExtra(Constants.KEY_SOURCE);
     ...

Now that our RestaurantDetailActivity can correctly determine if the user reached it from the SavedRestaurantListActivity or the RestaurantListActivity, let's alter the adapter accordingly. We'll include this source information as an additional argument to our new RestaurantPagerAdapter. The RestaurantPagerAdapter will then provide this information to the RestaurantDetailFragment.

RestaurantDetailActivity.java

public class RestaurantDetailActivity extends AppCompatActivity {
    private String mSource;
...
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mSource = intent.getStringExtra(Constants.KEY_SOURCE);

        // This line should already exist, we're just adding mSource as an additional parameter:
        adapterViewPager = new RestaurantPagerAdapter(getSupportFragmentManager(), mRestaurants, mSource);
...

As you may have guessed, we now need to ensure our RestaurantPagerAdapter is setup to accommodate this extra parameter.

RestaurantPagerAdapter.java
public class RestaurantPagerAdapter extends FragmentPagerAdapter {
    private ArrayList<Restaurant> mRestaurants;
    private String mSource;

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

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

Here, the RestaurantPagerAdapter simply declares the member variable, defines it in RestaurantPagerAdapter() , and then provides it as an additional parameter to the RestaurantDetailFragment's newInstance() method called in getItem().

We'll now need to ensure the RestaurantDetailFragment's newInstance() method may also accommodate this additional parameter:

RestaurantDetailFragment.java

public class RestaurantDetailFragment extends Fragment implements View.OnClickListener {
...
private String mSource;

    public static RestaurantDetailFragment newInstance(ArrayList<Restaurant> restaurants, Integer position, String source) {
        ...
        args.putString(Constants.KEY_SOURCE, source);
        ...
    }
...

Here, we've also included it as an additional parameter to the newInstance() method.

Hiding and Showing Fragment Elements

Now that this information is being successfully passed to any RestaurantDetailFragments, we can finally use it to impact the appearance of our menu in the RestaurantDetailFragment.

First, we'll snag the source information in the onCreate() method, and call setHasMenuOptions(true) to invoke the menu items in our Fragment class:

RestaurantDetailFragment.java
...
    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        mSource = getArguments().getString(Constants.KEY_SOURCE);
        setHasOptionsMenu(true);   
     }
  ...

Next, in the onCreateView() method, we'll hide or show the "Save Restaurant" button depending on the value of mSource:

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

        if (mSource.equals(Constants.SOURCE_SAVED)) {
            mSaveRestaurantButton.setVisibility(View.GONE);
        } else {
            // This line of code should already exist. Make sure it now resides in this conditional: 
            mSaveRestaurantButton.setOnClickListener(this);
        }
...

Here, if the user is viewing the RestaurantDetailFragment after having come from the SavedRestaurantListActivity (ie: they're viewing details for restaurants in their "Saved Restaurants" list) we will hide the "Save Restaurant" button entirely. If they're coming from anywhere else (the RestaurantListActivity, in our case) we will continue to attach the appropriate click listener to the button.

Now, if we run the application, and view our saved restaurants, we should see that we're no longer offered an option to save a restaurant we've already saved.

no-save-restaurant-button-portrait

no-save-restaurant-button-landscape

In the next lesson we'll make use of our new ability to pass our RestaurantDetailFragment information regarding the user's navigational context in order to hide or show a photo option in the menu, and offer users the ability to take and save photos of their visit to a saved restaurant!


Example GitHub Repo for MyRestaurants

Overview


  • Essentially, to hide and show different elements of a re-usable fragment depending on where the user is viewing said fragment, we can include data regarding the user's location in intent extras, and hide elements based on this information.

  • To hide a View, call setVisibility() with the parameters View.GONE. Example: mSaveRestaurantButton.setVisibility(View.GONE);

Examples