The Road Thus Far
So, previously I covered that the RecyclerView
is a replacement for the ListView
in Android that increases efficiency by “recycling” the view objects that are currently visible. While that’s great for efficiency, usability becomes more of a challenge as RecyclerView
s are more divorced from the data they represent than ListView
s were. Further, I demonstrated the ItemViewHolder
pattern, which creates a data structure in which your view is contained and modified as data is swapped in and out while the user scrolls.
Android’s RecyclerView and Showing Data Changes
I demonstrated some extremely simple CRUD functionality with a button which allows us to add to our RecyclerView
and update the view to display all contained items. However, what happens when one wants to do the other CRUD functionality? What if someone wants to update or delete a record and reflect that in the RecyclerView
in real time? One can always modify the underlying dataset and reset the adapter on your RecyclerView
. This leads to a bad user experience, though, since the view will reset back up to the top. Additionally, how do we interact by touching individual items? Since we don’t have an onItemClickListener
(which makes sense, since the “item” in play may change every time the view is recycled), there doesn’t seem to be a convenient way to get the item out to work on it.
For updating, the ViewHolder
has all of the access we need; but, deleting requires that we update the view of the whole set, not the one individual item. The solution to this issue is to flip the problem on its head and use an interface to inform some other object that data has changed and the RecyclerView
must be updated to reflect it. This other object must have the scope to call methods on the RecyclerView
. Our Activity
is a good candidate to be the arbiter of this activity.
Updating Using ViewHolder as a Click Listener
For updates, it makes sense that the tapped view’s ViewHolder
could handle the the click event. Is has access to redraw the view based on new data and could access the data to update it. Let’s implement the onClickListener
interface to perform updates on a clicked view’s data from the ViewHolder
.
public class SomeModelRecyclerViewAdapter extends RecyclerView.Adapter<SomeModelRecyclerViewAdapter.ViewHolder>{ private List<SomeModel> data; public SomeModelRecyclerViewAdapter(List<SomeModel> data) { this.data = data; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.model_item, parent, false); ViewHolder holder = new ViewHolder(view); view.setOnClickListener(holder); return holder; } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.someModel = data.get(position); holder.bindData(); } @Override public int getItemCount() { return data.size(); } public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); SomeModel someModel; TextView modelNameLabel; TextView modelDateLabel; public SomeModel getSomeModel() { return someModel; } public void setSomeModel(SomeModel someModel) { this.someModel = someModel; } public ViewHolder(View itemView) { super(itemView); } public void bindData() { if (modelNameLabel == null) { modelNameLabel = (TextView) itemView.findViewById(R.id.modelNameLabel); } if (modelDateLabel == null) { modelDateLabel = (TextView) itemView.findViewById(R.id.modelDateLabel); } modelNameLabel.setText(someModel.name); modelDateLabel.setText(dateFormat.format(someModel.addedDate)); } @Override public void onClick(View v) { someModel.addedDate = new Date(); someModel.save(); bindData(); } } }
So, here’s what has changed. The ViewHolder
now implements View.OnClickListener
. The implementation updates the record by changing the date to right now, saving the updated data, and re-binding the view.
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { ... @Override public void onClick(View v) { someModel.addedDate = new Date(); someModel.save(); bindData(); } }
In addition, the RecyclerViewAdapter
now sets the ViewHolder
as the View
‘s OnClickListener
after it creates the ViewHolder
instance.
public class SomeModelRecyclerViewAdapter extends RecyclerView.Adapter<SomeModelRecyclerViewAdapter.ViewHolder>{ ... @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.model_item, parent, false); ViewHolder holder = new ViewHolder(view); view.setOnClickListener(holder); return holder; } ... }
Dynamically Showing Deletions in Android’s RecyclerView
Reflecting the update of a single data item wasn’t quite as easy as adding one to the dataset; but, deleting an item presents new challenges still. We don’t want to delete the item and reset the whole UI, forcing the user to start at the top of what might be a very large list. Instead, we need an outside control structure with scope access to our RecyclerView
to handle a deletion event and inform the UI to update.
To handle the messaging when an item is deleted, we’ll create a new interface called the SomeModelDeletedListener
. This interface will enforce a single method: onSomeModelDeleted
, which takes the model that has been deleted and the position in the RecyclerView
that it currently occupies (and, hence, must vacate). It will be the responsibility of the assigned instance of the SomeModelDeletedListener
to update the UI to reflect the data change.
public interface SomeModelDeletedListener { void onSomeModelDeleted(SomeModel model, int position); }
Since our Android Activity
handles our RecyclerView
and other UI, it might make sense to delegate this responsibility to it. (Note, if you are using asynchronous calls via AsyncTask
or thread handlers, the activity may no longer be the proper place to handle this event. This is just the simplest demonstration possible.) When the Android Activity
receives a call that lets us know our item has been deleted, it should update the RecyclerView
to illustrate this data change.
To tie it all together, we’ll add an onLongClickListener
to allow our users to long press to delete data. The ViewHolder
can handle the deletion from the database and notify the SomeModelDeletedListener
that it had made that change.
Whoo, that’s a lot. Let’s put it into practice.
Let’s start at the long-press, which we’ll again handle in the RecyclerViewAdapter.ViewHolder
. Here’s what we add to the class:
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { ... SomeModelDeletedListener someModelDeletedListener; public SomeModelDeletedListener getSomeModelDeletedListener() { return someModelDeletedListener; } public void setSomeModelDeletedListener(SomeModelDeletedListener someModelDeletedListener) { this.someModelDeletedListener = someModelDeletedListener; } ... public ViewHolder(View itemView) { this(itemView, null); } public ViewHolder(View itemView, SomeModelDeletedListener someModelDeletedListener) { super(itemView); this.someModelDeletedListener = someModelDeletedListener; } ... @Override public boolean onLongClick(View view) { if (someModel != null) { // Deletion from the database someModel.delete(); if (someModelDeletedListener != null) { someModelDeletedListener.onSomeModelDeleted(someModel, getAdapterPosition()); } } return true; } }
We’ve added an instance of our SomeModelDeletedListener
to the class to handle deletions and a getter and setter for convenience. We’ve also added a constructor which takes both a View
and a SomeModelDeletedListener
to initialize. The other constructor we’ve modified to take the new one into effect. Lastly, we’ve implemented the View.OnLongClick
method and added delete logic (including informing listener, if present) to occur when the user long-presses on the item.
We’ll also have to make some changes to the enclosing adapter to make sure all this get wired and passed through. Here are those changes:
public class SomeModelRecyclerViewAdapter extends RecyclerView.Adapter<SomeModelRecyclerViewAdapter.ViewHolder>{ ... private SomeModelDeletedListener someModelDeletedListener; public SomeModelDeletedListener getSomeModelDeletedListener() { return someModelDeletedListener; } public void setSomeModelDeletedListener(SomeModelDeletedListener someModelDeletedListener) { this.someModelDeletedListener = someModelDeletedListener; } ... public SomeModelRecyclerViewAdapter(List<SomeModel> data) { this(data, null); } public SomeModelRecyclerViewAdapter(List<SomeModel> data, SomeModelDeletedListener someModelDeletedListener) { this.data = data; this.someModelDeletedListener = someModelDeletedListener; } ... @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.model_item, parent, false); ViewHolder holder = new ViewHolder(view, someModelDeletedListener); view.setOnClickListener(holder); view.setOnLongClickListener(holder); return holder; } ... }
Similarly to the ViewHolder
, we’ve added an instance of the SomeModelDeletedListener
with getters, setters and proper constructors to handle it. Additionally, we’ve added the SomeModelDeletedListener
to our constructor call for the ViewHolder. Finally, we’ve also set the ViewHolder
as the LongClickListener
for the View
.
Lastly, we need to change our Activity
into a proper SomeModelDeletedListener
. Here are those changes:
public class MainActivity extends AppCompatActivity implements SomeModelDeletedListener{ ... @Override public void onSomeModelDeleted(SomeModel model, int position) { if (modelList != null) { SomeModelRecyclerViewAdapter adapter = (SomeModelRecyclerViewAdapter) modelList.getAdapter(); adapter.notifyItemRemoved(position); } } }
Here, we simply implement the interface and override the onSomeModelDeleted
method. This method will update the UI by notifying it’s adapter to remove the specific item.
Extra Credit – Updating the In-Memory Data When Deleting
If you made all of the changes above, everything should work properly most of the time. However, if your Android ever redraws your app without attempting to go back to the database and re-fetch, your deleted item will suddenly reappear in the list. What gives?
Remember that your RecyclerViewAdapter
has a List
of SomeModel
objects that it uses to draw. If you never update that list but redraw with the same data, your deleted items will magically reappear. The solution is to write a method which removes the item from the in-memory dataset and call it from your SomeModelDeletedListener
. (A hint here: our SomeModelDeletedListener
represents something that reacts to data being deleted from the database, not any of the in-memory structures.) Here’s the custom method example:
public class SomeModelRecyclerViewAdapter extends RecyclerView.Adapter<SomeModelRecyclerViewAdapter.ViewHolder>{ ... private List<SomeModel> data; ... public void removeItemAt(int position) { data.remove(position); } }
Now, to implement the behavior in the Android Activity
, we modify our onSomeModelDeleted
implementation:
public class MainActivity extends AppCompatActivity implements SomeModelDeletedListener{ ... @Override public void onSomeModelDeleted(SomeModel model, int position) { if (modelList != null) { SomeModelRecyclerViewAdapter adapter = (SomeModelRecyclerViewAdapter) modelList.getAdapter(); adapter.removeItemAt(position); adapter.notifyItemRemoved(position); } } }
While all of this code seems like a lot of work, it makes for an efficient handling of your views and a clean responsive UI, reflecting what the user expects to see with each action. The additional time is worth the cost, both because of the added efficiency of RecyclerView and because the old ListView method is considered deprecated by Android.