Проблема с использованием 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()