Проблема с использованием LoaderManager.LoaderCallbacks<Cursor> с пользовательским RecyclerView.Adapter

Я попытался загрузить список, используя ListView вместе с LoaderManager.LoaderCallbacks и пользовательским CursorAdapter, и он работает нормально. Но я пытаюсь сделать то же самое, используя RecyclerView вместе с пользовательским RecyclerView.Adapter, но я получаю эту проблему:

Я получаю список, отображаемый в первый раз, но когда я поворачиваю устройство, список исчезает.

введите описание изображения здесь

введите описание изображения здесь

Вот код, пожалуйста, посмотрите.

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 ответа

Решение

Вкратце, проблема в том, что после ротации вам вручают то же самое Cursor что вы ранее зациклились перед вращением, но вы не учитываете его текущую позицию.

Cursor отслеживает и поддерживает свою позицию в своем наборе записей, так как я уверен, что вы собрали из различных move*() методы, которые он содержит. При первом создании Cursor позиция будет установлена ​​прямо перед первой записью; то есть его позиция будет установлена ​​в -1,

При первом запуске приложения LoaderManager звонки onCreateLoader(), где твой CursorLoader создается, а затем заставляет его загрузить и доставить его Cursor с Cursor позиция в -1, На данный момент, while (cursor.moveToNext()) цикл работает так, как и ожидалось, так как первый moveToNext() вызов переместит его на первую позицию (индекс 0), а затем до каждой доступной позиции после этого, до конца.

Однако при вращении LoaderManager определяет, что у него уже есть запрошенный Loader (определяется по идентификатору), который сам видит, что у него уже есть соответствующий Cursor загружен, так что просто сразу же доставляет то же самое Cursor объект снова (Это главная особенность Loader framework - он не будет перезагружать ресурсы, которые у него уже есть, независимо от изменений конфигурации.) В этом суть проблемы. Тот Cursor был оставлен в последней позиции, в которую он был перемещен перед вращением; то есть в его конце. Следовательно, Cursor не могу moveToNext(), чтобы while цикл просто никогда не запускается вообще, после начального onLoadFinished() до вращения.

Самым простым решением с данной настройкой было бы вручную изменить положение Cursor сам. Например, в getItems(), изменить if в 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 не очень сложно. В качестве альтернативы, конечно, уже есть решения. (Например, возможно, этот, хотя я не могу ручаться за это, atm, я часто вижу, что доверенный коллега часто рекомендует его.)

  • Я бы также отметил, что родной Loader рамки устарели, в пользу более новых ViewModel / LiveData фреймворк архитектуры в библиотеках поддержки. Однако, похоже, что новейшая библиотека androidx имеет свои внутренние, улучшенные Loader рамки, которые являются простой оберткой вокруг сказал ViewModel / LiveData настроить. Это, кажется, хороший, простой способ использовать известные Loader конструкции, все еще извлекая выгоду из недавних усовершенствований архитектуры.

Вместо LoaderManager.initLoader() вызов LoaderManager.restartLoader()

Другие вопросы по тегам