Lesson Weekend

There’s just one final step before our drag-and-drop and swipe-to-delete features are fully complete. Currently, Firebase will not actually delete a restaurant from a user's list of "Saved Restaurants" if we dismiss it from the screen, nor will it automatically save the order of our Saved Restaurants. If we leave the SavedRestaurantListActivity, the order of restaurants will return to its original state. Unless we instruct it otherwise, Firebase returns our data in alphabetical order of node key names.

In this lesson we will add an index attribute to our Restaurant class that will allow us to save an individual restaurant's location in our "Saved Restaurant" list, and code to handle actually removing restaurants from a user's list of "Saved Restaurants" in the database if they choose to delete one through the UI.

Accessing Firebase after Gesture Interactions

First, let's tell our adapter what to do when an item is moved or deleted. We can do this by adding code to the onItemMove() and onItemDismiss() methods from the ItemTouchHelperAdapter interface the FirebaseRestaurantListAdapter is implementing:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...

    @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        notifyItemMoved(fromPosition, toPosition);
        return false;
    }

    @Override
    public void onItemDismiss(int position) {
        getRef(position).removeValue();
    }
}
  • We call the notifyItemMoved() method to notify our adapter that the underlying data has changed.

  • To delete the dismissed item from Firebase, we can call the getRef() method, passing in an item's position and the FirebaseRecyclerAdapter will return the DatabaseReference for the given object. We can then call the removeValue() method to delete that object from Firebase. Once deleted, the FirebaseRecyclerAdapter will automatically update the view.

Now if we run our app and drag an item, the adjacent items will move to make room. And if we swipe to delete an item, it is removed from the Firebase database!

Storing Index Values

You probably noticed that if we navigate away from the SavedRestaurantsListActivity and come back to it, the items that we previously dragged and dropped are returned to their original order. Let's fix this.

We'll start by creating a new property for our Restaurant object called "index". We will eventually use this new index property to order the items pulled from Firebase:

Restaurant.java
public class Restaurant {
    ...
    String index;

    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.index = "not_specified";
    }

    public String getIndex() {
        return index;
    }

    public void setIndex(String index) {
        this.index = index;
    }
}

Instead of int, we give each restaurant a string index so that we may set the initial value to a string key in our object constructor. Alphabetically, numbers come before letters. So anytime we add a brand new restaurant to our list, it will receive the default string index value. Then, when we re-order our restaurants with our new drag-and-drop feature, we will overwrite this string index with a numerical index. So, any ordered restaurants will come in their numerical order, and new restaurants we add will automatically be added to the end of the list.

Next, let's add a new string to our Constants class so that we can reference the "index" key of our Restaurant objects when we go to sort them in our Query:

Constants.java
...
public static final String FIREBASE_QUERY_INDEX = "index";
...

Ordering By Index

Next, we will use the orderByChild() method to instruct the FirebaseRecyclerAdapter to return the Restaurant objects by index rather than by the order in which they appear in the database. To do this, we will need to create a Query object using the FirebaseDatabase and DatabaseReference (the FirebaseArrayAdapter accepts either a DatabaseReference or a Query). We will then pass this query into our FirebaseRestaurantListAdapter constructor in place of the DatabaseReference object:

SavedRestaurantsListActivity.java
public class SavedRestaurantListActivity extends AppCompatActivity implements OnStartDragListener {
    private DatabaseReference mRestaurantReference;
    private FirebaseRestaurantListAdapter mFirebaseAdapter;
    private ItemTouchHelper mItemTouchHelper;

    @Bind(R.id.recyclerView) RecyclerView mRecyclerView;

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

        setContentView(R.layout.activity_restaurants);
        ButterKnife.bind(this);

        setUpFirebaseAdapter();
    }

    private void setUpFirebaseAdapter() {

        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
        String uid = user.getUid();

        Query query = FirebaseDatabase.getInstance()
                .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                .child(uid)
                .orderByChild(Constants.FIREBASE_QUERY_INDEX);

        mFirebaseAdapter = new FirebaseRestaurantListAdapter(Restaurant.class,
                R.layout.restaurant_list_item_drag, FirebaseRestaurantViewHolder.class,
                query, this, this);
        ...
    }
    ...
}

Tracking Restaurant Indexes

How can we tell our database to update the index of each restaurant child every time a user drags and drops an item? Instead of trying to save each item’s index every time a user moves an item, let’s wait until a user is done with the SavedRestaurantListActivity and navigates away.

Each time a user leaves an activity, the onDestroy() method is automatically called. We'll override this method, and tell our app to trigger our adapter's cleanup()method. Then, we will override cleanup and include code to save the current order to Firebase.

We already added the code to call cleanup() in our SavedRestaurantListActivity, but make sure to trigger this method in all future projects:

SavedRestaurantListActivity,java
public class SavedRestaurantListActivity extends AppCompatActivity implements OnStartDragListener {
    ...

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mFirebaseAdapter.cleanup();
    }
}

Before we can properly store the index of the Restaurant items in our adapter's cleanup() method, we will first need to have access to the ArrayList of Restaurant objects at the given reference. Let's start by adding a ChildEventListener to grab out the Restaurants and save them to an ArrayList in our Adapter:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...
    private ChildEventListener mChildEventListener;
    private ArrayList<Restaurant> mRestaurants = new ArrayList<>();

    public FirebaseRestaurantListAdapter(Class<Restaurant> modelClass, int modelLayout,
                                         Class<FirebaseRestaurantViewHolder> viewHolderClass,
                                         Query ref, OnStartDragListener onStartDragListener, Context context) {
        super(modelClass, modelLayout, viewHolderClass, ref);
        mRef = ref.getRef();
        mOnStartDragListener = onStartDragListener;
        mContext = context;

        mChildEventListener = mRef.addChildEventListener(new ChildEventListener() {

            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                mRestaurants.add(dataSnapshot.getValue(Restaurant.class));
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });

    ...
}
  • Each time the adapter is constructed, the onChildAdded() will be triggered for each item in the given reference.

  • We will use the add() method to add each returned item to the mRestaurants ArrayList so that we can access the list of restaurants throughout our adapter.

To make sure that our mRestaurants ArrayList reflects the changes in the underlying data being tracked within FirebaseRecyclerAdapter, we need to update its' contents in the onItemMove() and onItemDismiss() overrides:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...

    @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        Collections.swap(mRestaurants, fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);
        return false;
    }

    @Override
    public void onItemDismiss(int position) {
        mRestaurants.remove(position);
        getRef(position).removeValue();
    }
    ...
}
  • We use Collections.swap() to update the order of our mRestaurants ArrayList items passing in the ArrayList of items and the starting and ending positions.

  • We call the remove() method on our ArrayList of items in onItemDismiss() to remove the item from mRestaurants at the given position.

Our mRestuarants ArrayList and the underlying data in the FirebaseRecyclerAdapter should now always be in sync.

Next, let's write a new method that we will eventually trigger in our adapter's cleanup() method. This new method will be in charge of re-assigning the "index" property for each restaurant object in our array list and then save it to Firebase:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...

    private void setIndexInFirebase() {
        for (Restaurant restaurant : mRestaurants) {
            int index = mRestaurants.indexOf(restaurant);
            DatabaseReference ref = getRef(index);
            restaurant.setIndex(Integer.toString(index));
            ref.setValue(restaurant);
        }
    }
}
  • We can grab the index of each restaurant in the mRestaurants ArrayList by calling the ArrayList.indexOf() method, passing in the object which we would like to know the index. We will use this index as the index in Firebase.

  • We grab the reference of each item using the getRef() method, passing in the position of the item in the ArrayList.

  • We then use the setIndex() method we added to our Restaurant model to update the index property for each item.

  • We can finally use the setValue() method passing the Restaurant object whose index property we just updated.

Finally, let's override that cleanup() method in our adapter. We will call our new setIndexInFirebase() method to update the index property for each Restaurant and remove the event listener: Remember, we don't want to overload our connection to the Firebase database by making a call to save the new index every time the onItemMove() method is called.

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...

    @Override
    public void cleanup() {
        super.cleanup();
        setIndexInFirebase();
        mRef.removeEventListener(mChildEventListener);
    }
}

If we run our app, drag and drop items, navigate away and the return to the SavedRestaurantListActivity you will see that the order of the items now persists. However, if we click an item to go to the DetailActivity, the app will bring us to the wrong page. This is because our click listener is inside of the view holder, but the view holder doesn't have access to the array list of restaurants in our adapter. Let's remove the click listener in our ViewHolder...

FirebaseRestaurantViewHolder.java
public class FirebaseRestaurantViewHolder extends RecyclerView.ViewHolder {
    private static final int MAX_WIDTH = 200;
    private static final int MAX_HEIGHT = 200;

    View mView;
    Context mContext;
    public ImageView mRestaurantImageView;

    public FirebaseRestaurantViewHolder(View itemView) {
        super(itemView);
        mView = itemView;
        mContext = itemView.getContext();
    }

    public void bindRestaurant(Restaurant restaurant) {
        mRestaurantImageView = (ImageView) mView.findViewById(R.id.restaurantImageView);
        TextView nameTextView = (TextView) mView.findViewById(R.id.restaurantNameTextView);
        TextView categoryTextView = (TextView) mView.findViewById(R.id.categoryTextView);
        TextView ratingTextView = (TextView) mView.findViewById(R.id.ratingTextView);

        Picasso.with(mContext)
                .load(restaurant.getImageUrl())
                .resize(MAX_WIDTH, MAX_HEIGHT)
                .centerCrop()
                .into(mRestaurantImageView);

        nameTextView.setText(restaurant.getName());
        categoryTextView.setText(restaurant.getCategories().get(0));
        ratingTextView.setText("Rating: " + restaurant.getRating() + "/5");
    }
}

..and instead, add a click listener to our adapter in the populateViewHolder() method:

FirebaseRestaurantListAdapter.java
...
@Override
    protected void populateViewHolder(final FirebaseRestaurantViewHolder viewHolder, Restaurant model, int position) {
        viewHolder.bindRestaurant(model);

        viewHolder.mRestaurantImageView.setOnTouchListener(new View.OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
                    mOnStartDragListener.onStartDrag(viewHolder);
                }
                return false;
            }

        });

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

            @Override
            public void onClick(View v) {
                Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
                intent.putExtra("position", viewHolder.getAdapterPosition());
                intent.putExtra("restaurants", Parcels.wrap(mRestaurants));
                mContext.startActivity(intent);
            }
        });
...
  • To get the current position of the click item, we can call the getAdapterPosition() method on the ViewHolder passed into the populateViewHolder() method.

  • Just like we did previously in the ViewHolder, we create an intent, pass in the position and the ArrayList of Restaurants and then call the startActivity() method using the context passed in to our constructor.

Notice that the position information we're including with our intent when we say intent.putExtra("position", viewHolder.getAdapterPosition()); is an integer. Make sure the RestaurantDetailActivity is prepared to gather a position of the integer data type when it receives this intent:

RestaurantDetailActivity.java
...
@Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        int startingPosition = getIntent().getIntExtra("position", 0);
       ...
    }
...

Now, if we launch our application we should be able to drag and drop items, navigate away from "Saved Restaurants" and see it maintains the order we've left it in! Additionally, if we delete a restaurant from our list, it should actually be removed from that user's list of "Saved Restaurants" in Firebase. Perfect!


Example GitHub Repo for MyRestaurants