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

时间:2018-11-10 11:58:45

标签: android android-recyclerview android-cursorloader android-loadermanager

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

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

enter image description here

enter image description here

这是代码,请看一看。

CatalogActivity

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

    }
}

ItemAdapter

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

我无法弄清楚确切的问题。请帮忙。

2 个答案:

答案 0 :(得分:2)

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

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()中,如果if不为null,则将moveToFirst()更改为Cursor,然后将while更改为do-while,因此我们不会无意间跳过第一条记录。那就是:

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

这样,当重新传递相同的Cursor对象时,其位置有点“重置”为位置0。由于该位置直接在第一个记录上,而不是在第一个记录之前(请记住,最初是-1),因此我们更改为do-while,这样第一个moveToNext()调用不会跳过Cursor中的第一条记录。


注意:

  • 我要提到的是,可以实现RecyclerView.Adapter以直接获取Cursor,这与旧的CursorAdapter类似。这样,Cursor必须在onBindViewHolder()方法中移动到每个项目的正确位置,并且不需要单独的ArrayList。会花费一些努力,但是将CursorAdapter转换为RecyclerView.Adapter并不是很困难。或者,当然已经有可用的解决方案。 (例如,可能是this one,尽管我不能担保它,但atm却经常看到受信任的用户经常推荐它。)

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

答案 1 :(得分:0)

代替LoaderManager.initLoader()调用LoaderManager.restartLoader()