【问题标题】:Logical pattern for listeners/observers to implement RecyclerView list and details activities侦听器/观察者实现 RecyclerView 列表和详细信息活动的逻辑模式
【发布时间】:2017-01-11 02:06:10
【问题描述】:

似乎有多种方法可以实现RecyclerView 列表,其中一些比其他更合乎逻辑。从一个简单的列表到数据更改的列表会增加复杂性。额外的复杂性来自实现查看列表项详细信息的功能。

虽然我在以这种方式实现列表方面取得了一些成功,但我觉得我想出的东西效率不高,也不是框架设计时的预期。看着我用过的各种方法,我一直在说“这不可能是他们希望我这样做的方式”。

我希望检查的基本应用程序是一个在可滚动列表中显示来自SQLite 数据库的记录的应用程序,让用户从列表中选择项目以查看详细信息,并让用户长按以切换属性一个物品。当然,显示应该通过各种视图、滚动、重新显示等保持一致。

此图显示了一个基本用例,它没有任何基础数据更改。蓝色的字是需要实施细节的区域。在这种情况下,“点击”需要将模型和位置放入详细活动中,可能是intent.putExtra()

与必须管理数据更改相比,上述功能相当简单。下面我们有一个场景,我们保持在同一个活动中,但是用户通过长按来更新数据:

聆听者或观察者的最佳位置在哪里?哪些对象需要注意?当然需要更新视图(如何)?如何管理对数据库的更新?我们如何确保正确地重绘视图?

下面我们有两个动作。长按类似于上面的长按,但是在详细活动中,我们是否使用模型列表的序列化副本进行操作?如果是这样,这些更改如何返回到“真实”模型和数据库?

谁在监听,传递了哪些参数,这些参数如何用于保持数据同步?在哪里放置监听器代码最合乎逻辑和可维护的地方是保持数据库和视图井然有序的代码?

框架的设计者打算用什么逻辑方法来处理这个看似简单的功能?应用程序类中应该有一些单实例霸主功能吗?我还没有看到这种事情的例子,但可能是一种选择。

如果结果证明这很“简单”,我会感到惊讶,但它肯定比我一直在处理的复杂混乱要容易。

【问题讨论】:

    标签: android android-recyclerview onclicklistener observer-pattern android-viewholder


    【解决方案1】:

    在应用中从多个位置管理数据库的最简单方法之一是使用ContentProvider 并为数据库中的每个表指定content:// URI。

    维护“主”列表视图:

    • 假设您有一个名为“animals”的数据库表,通过ContentProvider 访问该表的URI 是content://myPackage/animals。对于您的RecyclerView 活动,在onCreate 中,您将在content://myPackage/animals URI 上启动CursorLoader
    • 假设您正确设计了ContentProvider(即插入、删除和更新调用以ContentProvider.notifyChange() 结尾),您的加载程序将在表更改时自动查询和重新查询数据库。从加载程序的onLoadFinished() 回调中,您获取它返回的光标并使用它更新您的回收器视图适配器。尽管解释有点简单,但这几乎是保持主列表更新的必要条件,即使在应用程序的其他部分对数据库进行了更改。

    处理列表中的点击/长点击:

    • 不一定有“最好的方法”来做到这一点,但在适配器的onCreateViewHolder() 方法中,我通常采用项目的基本视图并将包含该视图的ViewHolder 设置为其点击监听器。这是因为当项目被点击时,ViewHolder 知道关于它在列表中的位置的重要信息(使用getAdapterPosition()getItemId())。
    • 例如,如果用户长时间单击 ID 为 3 的项目,我将使用 ContentResolver.update()content://mypackage/animals/3 的 URI 更新数据库。更新成功后,主列表会自动重新查询数据库,并以 ID #3 的项目的新状态刷新列表。

    【讨论】:

    • 由于更新范围很窄(只需更新一个标志以显示或不显示图标),看起来ContentProvider 需要添加很多代码。更新我的回收器视图适配器背后的细节是我所追求的。
    • 你通常有你的ViewHolder 实现监听器吗?我的 ViewHolder 没有任何设置监听器的方法。有什么教程或原型代码可以指点我吗?
    • 了解 ContentProvider。但是,我要指出,ContentProviders 是为适应 SQLite 数据库而量身定制的,并且由于您已经在使用它,它更像是整合您的数据库代码而不是添加更多代码。另外,您在使用 Loaders 时可以免费获得自动请求。
    • 就监听器而言,我有时将其放在 ViewPager 中,有时将其放在 Adapter 中,具体取决于它们需要的复杂程度。在任何一种情况下,侦听器通常都需要知道单击的视图在适配器中的位置。你可以查看我使用的这个教程:bignerdranch.com/blog/… 给你一些想法。它也涵盖了 onClick 侦听器。
    • 再次阅读您的第一条评论后,我提出了一个问题。当您说您使用的是 SQL 数据库时,我假设您的适配器仅通过管理从数据库中查询的游标来处理其列表数据。如果是这种情况,我仍然认为 ContentProvider 会有所帮助......如果没有,那么可能还有其他选择。
    【解决方案2】:

    这是实现这组功能的一种方法。原始问题中描述的所有功能都已实现并可在此处获得:Sample Application on GitHub。另外,如果您有改进建议,请在此处或在 GitHub 上提出建议。

    通常,列表活动中有一个模型,该模型作为额外的传递到幻灯片活动中。幻灯片活动模型的更改很容易在数据库中反映,并且需要一些努力才能在用户按下后退按钮时让幻灯片活动的更改出现在列表活动中。

    主要活动onCreate 创建一个 SQLite 数据库适配器并将一些数据放入其中。它创建一个ArrayList<Model> 并使用它来构建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());
            }
        }
    ...
    }
    

    这是布局:

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

    RecyclerView.Adapter 是听众所在的位置。我听一下点击和长按。单击启动幻灯片活动,长按更改数据库,以便图标显示或不显示。当用户采取更改数据的操作时,我需要确保模型和数据库得到更新,并且我需要通知视图发生了某些变化。请注意,此实现仅管理实际更改的内容,而不是刷新所有内容。

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

    我不会把所有代码都放在这里,我只放一个子集,更多细节可以通过上面的 github 链接找到。但最后一条评论是关于该应用程序如何设计以获取在幻灯片活动期间更改的项目列表。每次幻灯片活动发生更改时,位置都会在数据库更新的同时保存。然后,当幻灯片活动结束时,onActivityResult() 拉出所有这些位置并更新了原始活动中保存的模型。

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

    以下是一些与数据库交互的类。我决定反对ContentProvider,因为根据文档,这是为了在应用程序之间而不是在一个应用程序内共享数据。

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

    【讨论】:

    • 在第一个活动中,整个数据库被加载到modelList,然后作为可序列化传递给下一个活动。这是个好主意吗?
    • 这可能取决于整个数据库的大小。如果数据库是 100kb 怎么办?
    • 上述实现对于较大的数据集不是很好的做法。考虑实现RecyclerViewCursorAdapter,如图所示codereview.stackexchange.com/a/121358/123118quanturium.github.io/2015/04/19/…
    猜你喜欢
    • 2016-04-21
    • 2014-07-09
    • 1970-01-01
    • 1970-01-01
    • 2011-03-22
    • 2015-02-12
    • 2011-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多