I have tried loading the list using the ListView along with LoaderManager.LoaderCallbacks and custom CursorAdapter and it works fine. But I am trying to accomplish the same using RecyclerView along with custom RecyclerView.Adapter but I am getting this issue:
I am getting the list displayed for the first time but when I rotate the device the list disappears.
Here is the code, please have a look.
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();
}
}
}
I am not able to figure out the extact issue. Please help.
Briefly, the issue here is that, after rotation, you're being handed the same Cursor
that you had previously looped over before the rotation, but you're not accounting for its current position.
A Cursor
tracks and maintains its own position within its set of records, as I'm sure you've gathered from the various move*()
methods it contains. When first created, a Cursor
's position will be set to right before the first record; i.e., its position will be set to -1
.
When you first start your app, the LoaderManager
calls onCreateLoader()
, where your CursorLoader
is instantiated, and then causes it to load and deliver its Cursor
, with the Cursor
's position at -1
. At this point, the while (cursor.moveToNext())
loop works just as expected, since the first moveToNext()
call will move it to the first position (index 0
), and then to each available position after that, until the end.
Upon rotation, however, the LoaderManager
determines that it already has the requested Loader
(determined by ID), which itself sees that it already has the appropriate Cursor
loaded, so it just immediately delivers that same Cursor
object again. (This is a major feature of the Loader
framework – it won't reload resources it already has, regardless of configuration changes.) This is the crux of the issue. That Cursor
has been left at the last position to which it was moved before the rotation; i.e., at its end. Consequently, the Cursor
cannot moveToNext()
, so that while
loop just never runs at all, after the initial
onLoadFinished()
, before rotation.
The simplest fix, with the given setup, would be to manually reposition the Cursor
yourself. For example, in getItems()
, change the if
to moveToFirst()
if the Cursor
is not null, and change the while
to a do-while
, so we don't inadvertently skip over the first record. That is:
if (cursor != null && cursor.moveToFirst()) {
do {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
...
} while (cursor.moveToNext());
}
With this, when that same Cursor
object is re-delivered, its position is kinda "reset" to position 0
. Since that position is directly on the first record, rather than right before it (remember, initially -1
), we change to a do-while
, so that the first moveToNext()
call doesn't skip the first record in the Cursor
.
Notes:
I would mention that it is possible to implement a RecyclerView.Adapter
to take a Cursor
directly, similar to the old CursorAdapter
. In this, the Cursor
would necessarily be moved in the onBindViewHolder()
method to the correct position for each item, and the separate ArrayList
would be unnecessary. It'd take a little effort, but translating CursorAdapter
to a RecyclerView.Adapter
isn't terribly difficult. Alternatively, there are certainly solutions already available. (For example, possibly, this one, though I cannot vouch for it, atm, I often see a trusted fellow user recommend it often.)
I would also mention that the native Loader
framework has been deprecated, in favor of the newer ViewModel
/LiveData
architecture framework in support libraries. However, it appears that the newest androidx library has its own internal, improved Loader
framework which is a simple wrapper around said ViewModel
/LiveData
setup. This seems to be a nice, easy way to utilize the known Loader
constructs while still benefiting from the recent architecture refinements.