将 LoaderManager.LoaderCallbacks<Cursor> 与自定义 RecyclerView.Adapter 一起使用时出现问题

Problem using LoaderManager.LoaderCallbacks<Cursor> with custom RecyclerView.Adapter

我尝试使用 ListView 以及 LoaderManager.LoaderCallbacks 和自定义 CursorAdapter 加载列表,它工作正常。但我正在尝试使用 RecyclerView 和自定义 RecyclerView.Adapter 来完成相同的任务,但我遇到了这个问题:

我是第一次显示列表,但是当我旋转设备时列表消失了。

这是代码,请看一下

目录活动

public class CatalogActivity extends AppCompatActivity implements ItemAdapter.OnItemClickListener,
        LoaderManager.LoaderCallbacks<Cursor> {
    private static final int ITEMS_LOADER_ID = 1;
    public static final String EXTRA_ITEM_NAME = "extra_item_name";
    public static final String EXTRA_ITEM_STOCK = "extra_item_stock";

    @BindView(R.id.list_items)
    RecyclerView mListItems;

    private ItemAdapter mItemAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_catalog);
        ButterKnife.bind(this);

        setupListItems();

        getLoaderManager().initLoader(ITEMS_LOADER_ID, null, this);
    }

    private void setupListItems() {
        mListItems.setHasFixedSize(true);
        LayoutManager layoutManager = new LinearLayoutManager(this);
        mListItems.setLayoutManager(layoutManager);
        mListItems.setItemAnimator(new DefaultItemAnimator());
        mListItems.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
        mItemAdapter = new ItemAdapter(getApplicationContext(), this);
        mListItems.setAdapter(mItemAdapter);
    }

    @Override
    public void OnClickItem(int position) {
        Intent intent = new Intent(this, EditorActivity.class);

        Item item = mItemAdapter.getItems().get(position);
        intent.putExtra(EXTRA_ITEM_NAME, item.getName());
        intent.putExtra(EXTRA_ITEM_STOCK, item.getStock());

        startActivity(intent);
    }

    private ArrayList<Item> getItems(Cursor cursor) {
        ArrayList<Item> items = new ArrayList<>();

        if (cursor != null) {
            while (cursor.moveToNext()) {
                int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
                int columnIndexName = cursor.getColumnIndex(ItemEntry.COLUMN_NAME);
                int columnIndexStock = cursor.getColumnIndex(ItemEntry.COLUMN_STOCK);

                int id = cursor.getInt(columnIndexId);
                String name = cursor.getString(columnIndexName);
                int stock = Integer.parseInt(cursor.getString(columnIndexStock));

                items.add(new Item(id, name, stock));
            }
        }

        return items;
    }

    @Override
    public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
        switch (loaderId) {
            case ITEMS_LOADER_ID: {
                String[] projection = {
                        ItemEntry._ID,
                        ItemEntry.COLUMN_NAME,
                        ItemEntry.COLUMN_STOCK
                };

                return new CursorLoader(
                        this,
                        ItemEntry.CONTENT_URI,
                        projection,
                        null,
                        null,
                        null
                );
            }
            default:
                return null;
        }
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        mItemAdapter.setItems(getItems(cursor));
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {

    }
}

项目适配器

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
    private ArrayList<Item> mItems;
    private OnItemClickListener mOnItemClickListener;
    private Context mContext;

    public ItemAdapter(Context context, OnItemClickListener onItemClickListener) {
        mOnItemClickListener = onItemClickListener;
        mContext = context;
    }

    public void setItems(ArrayList<Item> items) {
        if (items != null) {
            mItems = items;
            notifyDataSetChanged();
        }
    }

    public ArrayList<Item> getItems() {
        return mItems;
    }

    public interface OnItemClickListener {
        void OnClickItem(int position);
    }

    public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        @BindView(R.id.tv_item)
        TextView tv_item;

        @BindView(R.id.tv_stock)
        TextView tv_stock;

        public ItemViewHolder(@NonNull View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            int position = getAdapterPosition();
            mOnItemClickListener.OnClickItem(position);
        }
    }

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_inventory, parent, false);
        return new ItemViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder itemViewHolder, int position) {
        final Item item = mItems.get(position);

        itemViewHolder.tv_item.setText(item.getName());
        itemViewHolder.tv_stock.setText(mContext.getString(R.string.display_stock, item.getStock()));
    }

    @Override
    public int getItemCount() {
        if (mItems == null) {
            return 0;
        } else {
            return mItems.size();
        }
    }
}

我无法找出确切的问题。请帮忙。

而不是 LoaderManager.initLoader() 调用 LoaderManager.restartLoader()

简而言之,这里的问题是,在轮换之后,您得到的 Cursor 与您之前在轮换之前循环过的相同,但您没有考虑其当前位置。

A Cursor 跟踪并维护自己在其记录集中的位置,我相信您已经从它包含的各种 move*() 方法中了解到这一点。第一次创建时,Cursor的位置会设置在第一条记录之前;即,其位置将设置为 -1.

当您第一次启动您的应用程序时,LoaderManager 调用 onCreateLoader(),您的 CursorLoader 在其中被实例化,然后导致它加载并交付其 CursorCursor 的位置在 -1。此时,while (cursor.moveToNext()) 循环按预期工作,因为第一个 moveToNext() 调用会将其移动到第一个位置(索引 0),然后移动到之后的每个可用位置,直到最后。

然而,在旋转时,LoaderManager 确定它已经具有请求的 Loader(由 ID 确定),它本身看到它已经具有适当的 Cursor 加载,所以它会立即再次传送相同的 Cursor 对象。 (这是 Loader 框架的一个主要特点——它不会重新加载它已经拥有的资源,无论配置如何变化。)这就是问题的症结所在。那Cursor一直留在旋转前最后移动到的位置;即,在它的尽头。因此,Cursor 不能 moveToNext(),因此 while 循环根本就不会运行,在初始 onLoadFinished(),旋转前。

根据给定的设置,最简单的修复方法是您自己手动重新定位 Cursor。例如,在 getItems() 中,如果 Cursor 不为空,则将 if 更改为 moveToFirst(),并将 while 更改为 do-while,所以我们不会无意中跳过第一条记录。即:

if (cursor != null && cursor.moveToFirst()) { 
    do {
        int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
        ...
    } while (cursor.moveToNext());
}

有了这个,当相同的 Cursor 对象被重新传送时,它的位置有点 "reset" 到位置 0。由于该位置直接在第一条记录上,而不是在它之前(记住,最初是 -1),我们更改为 do-while,这样第一个 moveToNext() 调用就不会跳过Cursor.

中的第一条记录

备注:

  • 我要提到的是,可以实现 RecyclerView.Adapter 来直接获取 Cursor,类似于旧的 CursorAdapter。在这种情况下,Cursor 必须在 onBindViewHolder() 方法中移动到每个项目的正确位置,并且不需要单独的 ArrayList。虽然有点费力,但翻译CursorAdapter to a RecyclerView.Adapter isn't terribly difficult. Alternatively, there are certainly solutions already available. (For example, possibly, this one,虽然我不能保证,atm,我经常看到一个值得信赖的用户经常推荐它。)

  • 我还要提到原生 Loader 框架已经 deprecated, in favor of the newer ViewModel/LiveData architecture framework in support libraries。然而,最新的 androidx 库似乎有自己的内部改进的 Loader 框架,它是上述 ViewModel/LiveData 设置的简单包装器。这似乎是利用已知 Loader 结构的一种很好、简单的方法,同时仍然受益于最近的架构改进。