Logical pattern for listeners/observers to implement RecyclerView list and details activities

谁说我不能喝 提交于 2019-12-08 02:31:02

问题


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.


回答1:


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 is content://myPackage/animals. For your RecyclerView activity, in onCreate you would start a CursorLoader on the content://myPackage/animals URI.
  • Assuming you design your ContentProvider correctly (ie. insert, delete and update calls end with ContentProvider.notifyChange()), your loader will automatically query and requery the database any time the table changes. From the loader's onLoadFinished() 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 the ViewHolder containing the view as its on click listener. This is because when the item is clicked, the ViewHolder knows important information about where it is within the list (using getAdapterPosition() or getItemId()).
  • 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 of content://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.



回答2:


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!