Lesson Weekend

Now that our interfaces, ItemTouchHelper.Callback, and new list item layout are in place, let's implement the drag-and-drop feature. We will start by creating a new adapter class and then updating our ViewHolder and SavedRestaurantListActivity.

Class Inheritance

Last week we created an instance of the FirebaseUI's FirebaseRecyclerAdapter to listen and respond to changes in a specified node. However, in order to implement our new listener interfaces we will need to create custom adapter that inherits all functionality of the FirebaseRecyclerAdapter and also implements the ItemTouchHelperAdapter. Before we do this, let's briefly learn about inheritance, and how it works.

In Java (and many other languages) inheritance refers to a relationship between two classes in which one (child) class has all of methods and properties of the other (parent) class, while also containing unique methods and properties of its own.

For example, a Dog class might inherit from an Animalclass. The Dog class would have all of the methods and properties of an Animal but also the unique methods and properties of a Dog. To inherit from a class, we use the keyword extend, like so:

class Dog extends Animal {
    ...
}

In the example above, the class Dog is able to call all methods and content from Animal, since it has inherited it.

Custom Adapters with Class Inheritance

As mentioned, let's make a custom adapter that inherits all functionality of the FirebaseRecyclerAdapter, and also includes its own code implementing the ItemTouchHelperAdapter. This will allow us to handle both FirebaseRecyclerAdapter and drag-and-drop functionalities in the same adapter.

We'll start by creating the new class in the adapters sub-package called FirebaseRestaurantListAdapter. We will extend the FirebaseRecyclerAdapter to inherit its functionality. We will also implement the ItemTouchHelperAdapterinterface, and create a constructor:

adapters/FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    private DatabaseReference mRef;
    private OnStartDragListener mOnStartDragListener;
    private Context mContext;

    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;
    }
  • Just like we saw in SavedRestaurantsActivity, the FirebaseRecyclerAdapter requires the class of the data that will populate the RecyclerView, the layout we will inflate for each item, the ViewHolder class, and the database reference or query.

  • We also add the OnStartDragListener and the context to the constructor. The context will be needed when we eventually create an intent to navigate to the detail activity.

  • In order to set our mRef member variable to the correct datatype, we call getRef() on an instance of Query to return the DatabaseReference.

  • We will eventually set a TouchListener on the restaurant ImageView and use the OnStartDragListener to trigger to onStartDrag() callback.

Next, let's override methods from the interfaces being implemented:

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

    private DatabaseReference mRef;
    private OnStartDragListener mOnStartDragListener;
    private Context mContext;

    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;
    }

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

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

    @Override
    public void onItemDismiss(int position) {

    }
}
  • onItemMove() and onItemDismiss() override methods from the ItemTouchHelperAdapter interface.

  • populateViewHolder() comes from an interface included as part of the FirebaseRecyclerAdapter class.

Implement OnStartDragListener and Attach ItemTouchHelper.Callback

We now need to make several changes so our SavedRestaurantListActivity:

  • Use our new FirebaseRestaurantListAdapter in SavedRestaurantListActivity in place of our FirebaseRecyclerAdapter.

  • Implement the OnStartDragListener interface In order to pass the OnStartDragListener to our FirebaseRestaurantListAdapter constructor.

  • Create an instance of the ItemTouchHelper and ItemTouchHelper.Callback in order to attach the OnStartDragListener to our ViewHolder.

  • Move the construction of our mRestaurantReference into our setUpFirebaseAdapter() method.

After these changes our SavedRestaurantListActivity should look like this:

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();

        mRestaurantReference = FirebaseDatabase
                .getInstance()
                .getReference(Constants.FIREBASE_CHILD_RESTAURANTS)
                .child(uid);

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

        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(mFirebaseAdapter);

        ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mFirebaseAdapter);
        mItemTouchHelper = new ItemTouchHelper(callback);
        mItemTouchHelper.attachToRecyclerView(mRecyclerView);
    }

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

    @Override
    public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
        mItemTouchHelper.startDrag(viewHolder);
    }
}
  • We add the ItemTouchHelper as a member variable to that we can use it in the OnStartDragListener's onStartDrag() method.

  • In the FirebaseRestaurantListAdapter constructor, this refers to the OnStartDragListener and the Context. Both are necessary to construct an instance of a FirebaseRestaurantListAdapter.

  • The SimpleItemTouchHelperCallback takes an adapter as a parameter so we pass it the instance of the FirebaseRestaurantListAdapter we just created.

  • The ItemTouchHelper takes a ItemTouchHelper.Callback as an argument so we can pass it the instance of the SimpleItemTouchHelperCallback that we just created using our adapter.

  • To enable the interfaces to communicate with the necessary callbacks, we must attach the ItemTouchHelper to our RecyclerView using the attachToRecyclerView() method.

  • Finally, we call the startDrag() method on the instance of our ItemTouchHelper inside of the onStartDrag() override which will eventually send our touch events back to our SimpleItemTouchHelperCallback.

Attach OnStartDragListener

Next, let's create an OnTouchListener where we will attach our drag listener to the ViewHolder. Only the Restaurant ImageView will be drag-enabled.

Considering our ViewHolder contains each of the views in a given item view, it may make sense to do this in the ViewHolder itself. However, the OnStartDragListenerpassed in to our adapter cannot be sent to our ViewHolder with a constructor, since the FirebaseRecyclerAdapter handles the construction of the ViewHolder internally.

Instead, we need to grant our adapter access to the ImageView by declaring it as a public member variable, and then set its OnTouchListener in the adapter. We'll begin this process by tweaking code in our ViewHolder to make the ImageView public:

FirebaseRestaurantViewHolder.java
public class FirebaseRestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    ...
    public ImageView mRestaurantImageView;

    ...

    public void bindRestaurant(Restaurant restaurant) {
        mRestaurantImageView = (ImageView) mView.findViewById(R.id.restaurantImageView);
        ...

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

We can now set an OnTouchListener on the ViewHolder's ImageView and instruct our listeners to listen for a drag events when the user touches a restaurant's image. We will do this in the populateViewHolder() method of our adapter:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...
    private OnStartDragListener mOnStartDragListener;

    ...
    @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;
            }
        });
    }

    ...
}

Now if we run our app we can click the image of a list item in "Saved Restaurants" and drag it. In the next lesson we'll address altering our data based on user interactions; such as deleting a restaurant when the user swipes it off the screen, or re-ordering the appearance of restaurants in the "Saved Restaurants" list even after the activity is quit and re-visted.


Example GitHub Repo for MyRestaurants

Terminology


  • Class inheritance: In Java (and many other programming languages) one class can acquire the methods and content of another class (in addition to its own methods and content) if it inherits from it. To inherit from a class, we use the keyword extend (ie class Lion extends Feline { ...).

Example


Example GitHub Repo for MyRestaurants