There seems to be various ways to implement RecyclerView
lists, some more logical than others. Going beyond a simple list to one where the data changes increases the complexity. Additional complexity comes from implementing the ability to view the details of the list items.
Although I have had some success at implementing lists in this manner, I feel that what I've come-up with is not efficient and not what was intended when the framework was designed. Looking at the various methods I've used, I keep saying "this can't be the way they want me to do it".
The basic application I wish to examine is one that displays records from a SQLite
database in a scrollable list, let's a user select items from the list to see details, and lets a user long-click to toggle an attribute of an item. And of course, the display should remain consistent through the various views, scrolling, redisplays, etc.
This image shows a basic use-case that does not have any underlying data changes. The words in blue are areas where implementation details are needed. In this case, "click" would require getting the model and the position into the detail activity, perhaps with intent.putExtra()
.
The above functionality is fairly straight-forward when compared to having to manage changes to the data. Below we have a scenario where we remain in the same activity, but the user takes action to update the data using a long click:
Where is the best place for the listener or observer? What objects need attention? Certainly the view needs to be updated (how)? How will the update to the database be managed? How can we make sure that the view will be redrawn properly?
Below we have two actions. The long-click is similar to the long-click, above, but when in the detail activity, are we operating with a serialized copy of the model list? If so, how do those changes get back to the 'real' model and the database?
Who is listening, what parameters are delivered, and how are those parameters used to keep the data synchronized? Where is the most logical and maintainable place to put the listener code, the code that keeps the database and views all in order?
What was the logical approach, intended by the designers of the framework, for handing this seemingly straight-forward functionality? Should there be some single instance overlord function in the application class? I haven't seen examples with that kind of thing, but might be an option.
I'd be surprised if this turned-out to be 'easy', but it has just got to be easier than the convoluted mess that I've been working through.
One of the easiest ways to manage your database from multiple locations in an app is to use a ContentProvider
and designate content://
URI's for each table in your database.
To maintain the "master" list view:
- Suppose you have a database table called "animals" and the URI to access the table via the
ContentProvider
iscontent://myPackage/animals
. For yourRecyclerView
activity, inonCreate
you would start aCursorLoader
on thecontent://myPackage/animals
URI. - Assuming you design your
ContentProvider
correctly (ie. insert, delete and update calls end withContentProvider.notifyChange()
), your loader will automatically query and requery the database any time the table changes. From the loader'sonLoadFinished()
callback, you take the cursor it returns and update your recycler view adapter with it. Although a somewhat simplified explanation, this is pretty much the essentials to keep the master list updated even as changes to the database are made in other parts of the app.
To handle clicks/long clicks in the list:
- There is not necessarily a "best way" to do this, but in the adapter's
onCreateViewHolder()
method I usually take the base view for an item and set theViewHolder
containing the view as its on click listener. This is because when the item is clicked, theViewHolder
knows important information about where it is within the list (usinggetAdapterPosition()
orgetItemId()
). - If for example the user long clicked an item with an ID of 3, I would update the database using
ContentResolver.update()
and a URI ofcontent://mypackage/animals/3
. After the update was successful, the master list would then automatically requery the database and refresh the list with the new state for the item with ID #3.
Here is one way to implement this set of functionality. All of the functionality described in the original question is implemented and is available here: Sample Application on GitHub. Also, if you have suggestions for improvement, please advise, either here or on GitHub.
Generally, there is a model held in the list activity, and that model is passed as an extra into the slide activity. Changes to the slide activity model are easily reflected there and in the database, and a bit of effort was required to allow the changes in the slide activity to appear in the list activity when the user pressed the back button.
The main activity onCreate
creates an SQLite database adapter and puts some data in there. It creates an ArrayList<Model>
and uses that to build the RecyclerView.Adapter
:
public class RecyclerSqlListActivity extends AppCompatActivity {
....
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_list);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView1);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
try {
repository = new DbSqlliteAdapter(this);
repository.open();
//repository.deleteAll();
//repository.insertSome();
modelList = repository.loadModelFromDatabase();// <<< This is select * from table into the modelList
Log.v(TAG, "The modelList has " + modelList.size() + " entries.");
recyclerViewAdapter = new MyRecyclerViewAdapter(this, modelList);
recyclerView.setAdapter(recyclerViewAdapter);
recyclerView.hasFixedSize();
} catch (Exception e) {
Log.v(TAG, "Exception in onCreate " + e.getMessage());
}
}
...
}
Here's the layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</LinearLayout>
The RecyclerView.Adapter
is where the listeners are. I listen for a click and a long click. The click starts the slide activity and the long click changes the database so that the icon shows or not. When the user takes action that changes data, I need to make sure the model, and database get updated and I need to notify the view that something has changed. Note that this implementation just manages things that have actually changed, as opposed to refreshing everything.
class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView iconImageView;
TextView nameTextView;
TextView secondLineTextView;
TextView dbItemTextView;
TextView hiddenTextView;
TextView descriptionTextView;
ViewHolder(View itemView) {
super(itemView);
iconImageView = (ImageView) itemView.findViewById(R.id.bIcon);
nameTextView = (TextView) itemView.findViewById(R.id.bName);
secondLineTextView = (TextView) itemView.findViewById(R.id.bSecondLine);
dbItemTextView = (TextView) itemView.findViewById(R.id.bDbItem);
hiddenTextView = (TextView) itemView.findViewById(R.id.bHidden);
descriptionTextView = (TextView) itemView.findViewById(R.id.bDescription);
}
}
private List<Model> mModelList;
private Context mContext;
MyRecyclerViewAdapter(Context context, List<Model> modelList) {
mContext = context;
mModelList = new ArrayList<>(modelList);
}
@Override
public MyRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View modelView = inflater.inflate(R.layout.list_item, parent, false);
return new ViewHolder(modelView);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
final Model model = mModelList.get(position);
viewHolder.nameTextView.setText(model.getName());
viewHolder.secondLineTextView.setText(model.getSecond_line());
viewHolder.dbItemTextView.setText(model.getId() + "");
viewHolder.hiddenTextView.setText(model.getHidden());
viewHolder.descriptionTextView.setText(model.getDescription());
if ("F".equals(model.getHidden())) {
viewHolder.secondLineTextView.setVisibility(View.VISIBLE);
viewHolder.iconImageView.setVisibility(View.INVISIBLE);
} else {
viewHolder.secondLineTextView.setVisibility(View.INVISIBLE);
viewHolder.iconImageView.setVisibility(View.VISIBLE);
}
// DEFINE ACTIVITY THAT HAPPENS WHEN ITEM IS CLICKED
viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.v(TAG, "setOnClickListener fired with view " + view); // view is RelativeLayout from list_item.xml
int position = mModelList.indexOf(model);
Log.v(TAG, "position was : " + position);
Intent intent = new Intent(mContext, DetailSlideActivity.class);
intent.putExtra(DetailSlideActivity.EXTRA_LIST_MODEL, (Serializable)mModelList);
intent.putExtra(DetailSlideActivity.EXTRA_POSITION, position);
((Activity)mContext).startActivityForResult(intent, RecyclerSqlListActivity.DETAIL_REQUEST);
}
});
// If the item is long-clicked, we want to change the icon in the model and in the database
viewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
Log.v(TAG, "setOnLongClickListener fired with view " + view); // view is RelativeLayout from list_item.xml
Log.v(TAG, "setOnLongClickListener getTag method gave us position: " + view.getTag());
int position = mModelList.indexOf(model);
Log.v(TAG, "position was : " + position);
String hidden = model.getHidden();
Log.v(TAG, "hidden string was : " + hidden);
if ("F".equals(hidden)) {
model.setHidden("T");
DbSqlliteAdapter.update(model);
view.findViewById(R.id.bIcon).setVisibility(View.INVISIBLE);
} else {
model.setHidden("F");
view.findViewById(R.id.bIcon).setVisibility(View.VISIBLE);
}
Log.v(TAG, "updating the database");
DbSqlliteAdapter.update(model);
Log.v(TAG, "notifyItemChanged being called");
notifyItemChanged(position);
boolean longClickConsumed = true; // no more will happen :)
return longClickConsumed;
}
});
}
Rather than put all of the code here, I'll just put a subset, and more details can be found by going to the github link, above. But one last comment on how the app was designed to get the list of items that were altered during the slide activity. Each time a change happened in the slide activity, the position was saved at the same time the database was updated. Then, when the slide activity ended, onActivityResult()
pulled all of those positions and updated the model that is held in the original activity.
public class RecyclerSqlListActivity extends AppCompatActivity {
....
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if(requestCode == RecyclerSqlListActivity.DETAIL_REQUEST){
Log.v(TAG, "onActivityResult fired <<<<<<<<<< resultCode:" + resultCode);
//String[] changedItems = intent.getStringArrayExtra(RecyclerSqlListActivity.DETAIL_RESULTS); // We could return things we learned, such as which items were altered. Or we could just update everything
//modelList = repository.loadModelFromDatabase();// <<< This is select * from table into the modelList
Integer[] changedPositions = DbSqlliteAdapter.getChangedPositions();
for (Integer changedPosition : changedPositions) {
Model aModel = modelList.get(changedPosition);
DbSqlliteAdapter.loadModel(aModel);
recyclerViewAdapter.notifyItemChanged(changedPosition);
}
}
}
....
}
Here are a few of the classes that interact with the database. I decided against ContentProvider
because, according to the documentation, that's for sharing data between applications, not within one application.
class DbSqlliteAdapter {
....
static Model loadModel(Model model) {
Model dbModel = DbSqlliteAdapter.getById(model.getId() + "");
Log.v(TAG, "looked up " + model.getId() + " and it found " + dbModel.toString());
model.setId(dbModel.getId());
model.setName(dbModel.getName());
model.setSecond_line(dbModel.getSecond_line());
model.setDescription(dbModel.getDescription());
model.setHidden(dbModel.getHidden());
return model;
}
private static class DatabaseHelper extends SQLiteOpenHelper {....}
....
static void update(Model model) {
ContentValues values = fillModelValues(model);
Log.v(TAG, "Working on model that looks like this: " + model);
Log.v(TAG, "Updating record " + values.get(Model.ID) + " in the database.");
mDb.update(SQLITE_TABLE, values, Model.ID + "=?", new String[] {"" + values.get(Model.ID)});
Model resultInDb = getById("" + values.get(Model.ID));
Log.v(TAG, "after update, resultInDb: " + resultInDb);
}
static void update(Model model, int position) {
positionSet.add(position);
update(model);
}
static Integer[] getChangedPositions() {
Integer[] positions = positionSet.toArray(new Integer[0]);
positionSet.clear();
return positions;
}
....
}
来源:https://stackoverflow.com/questions/39301169/logical-pattern-for-listeners-observers-to-implement-recyclerview-list-and-detai