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

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.

Instead of LoaderManager.initLoader() call LoaderManager.restartLoader()

Related

Weird behaviour trying to retrieve data with SQLite while on AsyncTask

My app was very laggy, so I decided to use an AsyncTask to do the heaviest operations inside it and so, the app wouldn't be so slow at changing tabs.
But now, it is behaving in a very weird way. Let me explain: I have a ViewPager2, and inside that ViewPager, I have a recyclerview.
I put an AsyncTask inside the ViewPager, because it is the heaviest operation done in the fragment, and in the adapter of that ViewPager, I retrieve some values from a Database via a class called DatabaseHelper which one that extends SQLiteOpenHelper and has this method.
public Cursor getAllTasksByList(int ListID)
{
SQLiteDatabase db = this.getWritableDatabase();
Cursor c = db.rawQuery("SELECT * FROM " + Db.Tables.Tasktable.TASKS_TABLE + " WHERE " + Db.Tables.Tasktable.COL_LIST_ID + " = " + ListID, null);
return c;
}
Because the DatabaseHelper only returns one Cursor, I use another class to keep the code organized, this class takes the Cursor as argument and returns a list of "ListItem". This class is called "FolderUtils" and contains the following method (which one that I use to populate my RecyclerView inside that is inside my ViewPager):
public ArrayList<TaskItem> getTasksByList(int ListID, Context context) {
ArrayList<TaskItem> tasks = new ArrayList<>();
DatabaseHelper d = new DatabaseHelper(context);
Cursor c = d.getAllTasksByList(ListID);
while (c.moveToNext()) {
int id = c.getInt(0);
int listid = c.getInt(1);
boolean checked = c.getInt(2) > 0;
String title = c.getString(3);
tasks.add(new TaskItem(id, listid, checked, title));
}
return tasks;
}
But here it is the problem, sometimes this List is empty, but another times, it just retrieves the first value of the that Table I look for, strangely, sometimes it returns wrong values and it only works sometimes if I move my ViewPager to another position or if I just put some breakpoints. Here is my Adapter code.
#Override
public void onBindViewHolder(#NonNull ListHolder holder, int position) {
new LoadData(mList.get(position), holder).execute();
}
#Override
public int getItemCount() {
return mList.size();
}
private class LoadData extends AsyncTask<Void, Void, Void> {
private ListItem item;
private ListHolder holder;
public LoadData(ListItem item, ListHolder holder) {
this.item = item;
this.holder = holder;
}
#Override
protected void onPreExecute(){
super.onPreExecute();
//I set the visibility to GONE so that the user can just see the final layout and not the layout "Building" itself.
holder.itemView.setVisibility(View.GONE);
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
setItems(item, holder); //setItems is for setting the UI Content.
AttachRecycler(holder, item); //AttachRecycler creates an adapter for the recyclerview with the TaskList values, and attaches it to the recyclerview inside the ViewPager item.
holder.itemView.setVisibility(View.VISIBLE); //Shows the finished item
}
#Override
protected Void doInBackground(Void... voids) {
SetList(item); //SetList is where it takes the values from database and adds it to the list.
return null;
}
}
private void SetList(ListItem item) {
TaskList = new ArrayList<>();
else if (Mode == 1)
{
//Mode by default is 1. The line below does gets executed, however, it returns the wrong values.
TaskList.addAll(FolderUtils.getInstance().getTasksByList(item.getID(), context));
}
private void AttachRecycler(ListHolder holder, ListItem item)
{
LinearLayoutManager manager = new LinearLayoutManager(context);
holder.recycler.setLayoutManager(manager);
adapter = new TaskAdapter(TaskList, item.getColor(), context, item.getID());
holder.recycler.setAdapter(adapter);
}
How could I fix this? Thank You.
Solved this by myself.
Solution was to make TaskList a private variable inside the LoadData class, not a private variable of the entire Adapter, this acts like a local variable for every item instance, removing the duplicates in some items.

recycler Adapter & loaderManager, current viewing items go down when new items come in

I'm new to android.
I'm trying to create a recyclerAdapter in my app with loaderManager to load cursor from sqlite asynchronously.
When there is new data available from sqlite database, I want to insert new items on top of recyclerView. But at the same time, also want to maintain current viewing item.
Let's say, I'm currently viewing item range 5th to 10th, when new 10 items come in, current viewing position also should be 15th to 20th (not to new 5th to 10th).
Otherwise, if lots of items (50 to 60 items) insert, user will lose what he was previously looking at.
So, Is there any way that can maintain current viewing item with recyclerView and loaderManager? How Can it be done simply?
Edit
My code are still simple. Nothing complicated yet to mention specially. But if u insist, Here is my codes.
MyActivity.java
public class MyActivity extends BaseActivity
implements LoaderManager.LoaderCallbacks<Cursor>{
private CustomAdapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_layout);
mAdapter = new CustomAdapter();
RecyclerView recycler = (RecyclerView) findViewById(R.id.recycler_view);
recycler.setAdapter(mAdapter);
recycler.getItemAnimator().setAddDuration(1000);
recycler.getItemAnimator().setChangeDuration(1000);
recycler.setHasFixedSize(true);
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recycler.setLayoutManager(llm);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
if(mAdapter != null || mAdapter.getItemCount() != 0){
mAdapter.closeCursor();
}
super.onDestroy();
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this,buildDataUri(),null,null,null,null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
CustomAdapter.java
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
Cursor cursor;
public void swapCursor(Cursor c){
cursor = c;
if(c != null){
cursor.moveToFirst();
...
}
notifyDataSetChanged();
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
CardView c = (CardView) LayoutInflater.from(parent.getContext()).inflate(R.layout.card_cardView, parent, false);
return new ViewHolder(c);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
...
}
#Override
public int getItemCount() {
return cursor != null ? cursor.getCount() : 0;
}
public void closeCursor() {
cursor.close();
}
public static class ViewHolder extends RecyclerView.ViewHolder{
...
}
#Zoedia I think its how it works, when new items comes in obviously your current items will scroll down.
You can try a work around by not displaying new items in Recycler View as it comes while giving a Floating Button from top and asking user to click on button or Swipe-to-Refresh to load new items. This will help you read the stuff you are currently reading and new items will be displayed (Recycler View will be inflated with new Items) as soon as user click on that button.
Its similar to Facebook Android App implementation (New Stories Button) :
Edit 1:
To add an item at a particular position, you have to add items from your loader at the 0th/initial position as you want the new feed to be at top.
After that call the mAdapter.notifyDataSetChanged(); to reflect that changes in Recycler view with new elements.
If you were using the ArrayList<>, then you just have to call .add(listitem,position) method to add a single item or .addAll(list,position) to add all the ArrayList.
and then call mAdapter.notifyDataSetChanged();.

RecyclerView Image background change randomly

I've implemented a RecyclerView with CardView. Each CardView has an ImageView which I want to change its background color depending on the result of a Query (Empty result set -> Grey / Non Empty result set -> Red), this is implemented on the onBindViewHolder of the RecyclerView Adapter.
Here's the Adapter's Code (I've removed most of the code for the sake of clarity):
public class FavDirsAdapter extends RecyclerView.Adapter<FavDirsAdapter.FavDirsViewHolder> {
private LayoutInflater mInflater;
private Cursor mCursor;
private Context mContext;
private int range;
private FragmentManager mFragmentManager;
public FavDirsAdapter(Context context, Cursor cursor, FragmentManager fm) {
mInflater = LayoutInflater.from(context);
mCursor = cursor;
mContext = context;
range = cursor.getCount();
mFragmentManager = fm;
}
#Override
public FavDirsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = mInflater.inflate(R.layout.item_fav_dirs_list, parent, false);
return new FavDirsViewHolder(view);
}
#Override
public void onBindViewHolder(final FavDirsViewHolder viewHolder, final int position) {
if (mCursor.moveToFirst()) {
mCursor.moveToPosition(position);
viewHolder.favDirsItemTextView.setText(mCursor.getString(mCursor.getColumnIndex(
(FilmoContract.FavDirEntry.COLUMN_DIR))));
getDirImage(viewHolder);
setScheduledFilmsColor(viewHolder);
}
}
#Override
public int getItemCount() {
if (mCursor.moveToFirst()) {
return range;
}
return 0;
}
class FavDirsViewHolder extends RecyclerView.ViewHolder{
TextView favDirsItemTextView;
ImageView favDirsItemImageView;
ImageView favDirsItemScheduledFilmsImage;
public FavDirsViewHolder(View itemView) {
super(itemView);
favDirsItemTextView = (TextView) itemView.findViewById(R.id.fav_dirs_item_text_view);
favDirsItemImageView = (ImageView) itemView.findViewById(R.id.fav_dirs_item_image_view);
favDirsItemScheduledFilmsImage =
(ImageView) itemView.findViewById(R.id.fav_dirs_item_scheduled_films_image_button);
}
}
private void setScheduledFilmsColor(FavDirsViewHolder viewHolder) {
new SetScheduledFilmsColor(viewHolder, mContext).execute();
}
private void getDirImage (FavDirsViewHolder viewHolder) {
new FetchFavDirsImage(viewHolder, mContext).execute();
}
}
The adapter receive a Cursor with a Films Director data list stored on a DB. Then assign each row data to a ViewHolder fileds.
Then I make an additional query to determine wheter each Director has movies scheduled soon, and depending on the query result, color the ImageView (favDirsItemScheduledFilmsImage) background.
I tried first to make the query on the onBindViewHolder Adapter's method, but I've found that all the ImageViews, where colored in the same color (Grey). So since there is a DB Query involved, I've tried to create an AsyncTask (SetScheduledFilmsColor) to do all that stuff. Here's the code:
public class SetScheduledFilmsColor extends AsyncTask<Void, Void, Boolean> {
private ImageView mImageButton;
private String mDirName;
private Context mContext;
boolean scheduledFilms;
static final String[] PROGRAM_COLUMNS = {
FilmoContract.FilmEntry._ID,
FilmoContract.FilmEntry.COLUMN_DATE,
FilmoContract.FilmEntry.COLUMN_TIME,
FilmoContract.FilmEntry.COLUMN_CYCLE,
FilmoContract.FilmEntry.COLUMN_TITLE,
};
public SetScheduledFilmsColor(FavDirsAdapter.FavDirsViewHolder viewHolder, Context context) {
mImageButton = viewHolder.favDirsItemScheduledFilmsImage;
//mImageButton = new ImageButton(mContext);
mDirName = viewHolder.favDirsItemTextView.getText().toString();
mContext = context;
}
#Override
protected Boolean doInBackground(Void... params) {
Uri filmoDirector = FilmoContract.FilmEntry.buildProgramUriWithDirector();
Cursor tempCursor = mContext.getContentResolver().query(
filmoDirector,
PROGRAM_COLUMNS,
mDirName,
null,
null
);
scheduledFilms = tempCursor.moveToFirst();
tempCursor.close();
return scheduledFilms;
}
#Override
protected void onPostExecute(Boolean scheduledFilms) {
super.onPostExecute(scheduledFilms);
mImageButton.setImageResource(R.drawable.ic_new_releases_white_24dp);
if (scheduledFilms) {
//mImageButton.getBackground().clearColorFilter();
mImageButton.getBackground().setColorFilter(
mContext.getResources().getColor(R.color.lafilmo_color), PorterDuff.Mode.MULTIPLY
);
} else {
//mImageButton.getBackground().clearColorFilter();
mImageButton.getBackground().setColorFilter(
mContext.getResources().getColor(R.color.dividers), PorterDuff.Mode.MULTIPLY
);
}
}
}
I'm changing the color in the onPostExecute method. However, even doing this asynchronously, it doesn't matter the query result (I'm sure the query and the onPostExecute condition are fine, I have debug that). Even more, each time I reload the RecyclerView Fragment while navigating through my App, the colored ImageViews are different each time.
I don't understand, how RecyclerView can maintain the reference correctly to the other CardView fields (the fileds on the viewHolder, like favDirsItemTextView or favDirsItemImageView) which are assigned on the onBindViewHolder, and not maintain a reference to the colored ImageView (favDirsItemScheduledFilmsImage).
Can anybody shed some light on this? Is there a better way to do this?
Thanks!

Implemented Android AlphabetIndexer but it's not showing

I have followed a few stackoverflow threads, tutorials and what I can gather from the documentation but just can't get the AplhabetIndexer working in Android. The goal is to have an indexed ListView that users can quickly scroll using the letters on the right as per the standard contacts app on your phone. Eventually I'll add section headers in the list and make it filterable as a user types but for now I just want to get the basic list working.
I can load the list and get all my results from the cursor, but I never get the letters appear on the right of the ListView. I've tried different combinations of setting the adapter, including in the onCreateView with a null cursor and then calling changeCursor(cursor) in the onLoadFinished() callback, as well as the current version below which sets up the adapter completely in the onLoadFinished() callback.
Has anyone got a full working version of their setup and adapter code they could share? Preferably using the method of creating the adapter first, then just calling changeCursor(cursor) in the onLoadFinished() callback.
What I have so far:
StoreListAdapter.java
public class StoreListAdapter extends SimpleCursorAdapter implements SectionIndexer {
private AlphabetIndexer mAlphabetIndexer;
public StoreListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
if(cursor != null){
mAlphabetIndexer = new AlphabetIndexer(cursor,
cursor.getColumnIndex(StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME),
"ABCDEFGHIJKLMNOPQRTSUVWXYZ");
mAlphabetIndexer.setCursor(cursor);
}
}
#Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
if(cursor != null){
mAlphabetIndexer = new AlphabetIndexer(cursor,
cursor.getColumnIndex(StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME),
"ABCDEFGHIJKLMNOPQRTSUVWXYZ");
mAlphabetIndexer.setCursor(cursor);
}
}
#Override
public Object[] getSections() {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getSections();
}else{
return null;
}
}
#Override
public int getPositionForSection(int sectionIndex) {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getPositionForSection(sectionIndex);
}else{
return 0;
}
}
#Override
public int getSectionForPosition(int position) {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getSectionForPosition(position);
}else{
return 0;
}
}
}
StoreListFragment.java
public class StoreListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private ListView mListView;
private StoreListAdapter mAdapter;
public static StoreListFragment newInstance() {
StoreListFragment fragment = new StoreListFragment();
return fragment;
}
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public StoreListFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_store_search, container, false);
mListView = (ListView) view.findViewById(R.id.search_result_list);
return view;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
}
#Override
public void onDetach() {
super.onDetach();
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
getActivity(), // Parent activity context
StoreProvider.CONTENT_URI, // Table to query
null, // Projection to return
null, // No selection clause
new String[]{getString(R.string.centre_id)}, // No selection arguments
null // Default sort order
);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mListView.setFastScrollEnabled(true);
mListView.setScrollingCacheEnabled(true);
mAdapter = new StoreListAdapter(getActivity().getApplicationContext(), R.layout.store_list_item, data, new
String[]{StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME}, new int[]{R.id.item_name}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
mListView.setAdapter(mAdapter);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.changeCursor(null);
}
}
The exact behavior can be found in class FastScroller which is a helper class for AbsListView. There is a piece of code there that decides if "the list is long"
final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
MIN_PAGES is defined with value of 4. There you have it, if your list item count is not at least 4x the child count (visible rows) fast scroller and thus alphabet indexer will not appear.
Actually, when I added more test data it started working. Would appear that if your search results are small (I was using about 20 or so) it doesn't kick in. Once I added some dummy data of about 100 or so then it started working.

How is the position of a RecyclerView adapter related to the index of its dataset?

I thought they were the same, but they're not. The following code gives an indexOutOfBounds exception when I try to access the "position" index of my dataset, in this case a list of a model I created called Task:
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.TaskViewHolder> {
private List<Task> taskList;
private TaskAdapter thisAdapter = this;
// cache of views to reduce number of findViewById calls
public static class TaskViewHolder extends RecyclerView.ViewHolder {
protected TextView taskTV;
protected ImageView closeBtn;
public TaskViewHolder(View v) {
super(v);
taskTV = (TextView)v.findViewById(R.id.taskDesc);
closeBtn = (ImageView)v.findViewById(R.id.xImg);
}
}
public TaskAdapter(List<Task> tasks) {
if(tasks == null)
throw new IllegalArgumentException("tasks cannot be null");
taskList = tasks;
}
// onBindViewHolder binds a model to a viewholder
#Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
final int position = pos;
Task currTask = taskList.get(pos);
taskViewHolder.taskTV.setText(currTask.getDescription());
**taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d("TRACE", "Closing task at position " + position);
// delete from SQLite DB
Task taskToDel = taskList.get(position);
taskToDel.delete();
// updating UI
taskList.remove(position);
thisAdapter.notifyItemRemoved(position);
}
});**
}
#Override
public int getItemCount() {
//Log.d("TRACE", taskList.size() + " tasks in DB");
return taskList.size();
}
// inflates row to create a viewHolder
#Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int pos) {
View itemView = LayoutInflater.from(parent.getContext()).
inflate(R.layout.list_item, parent, false);
Task currTask = taskList.get(pos);
//itemView.setBackgroundColor(Color.parseColor(currTask.getColor()));
return new TaskViewHolder(itemView);
}
}
Deleting from my recyclerview gives unexpected results sometimes. Sometimes the element ahead of the one clicked is deleted, other times an indexOutOfBounds exception occurs at "taskList.get(position)".
Reading https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html and https://developer.android.com/training/material/lists-cards.html did not give me any more insight into why this was happening and how to fix it.
It looks like RecyclerView recycles the rows, but I wouldn't expect an indexoutofbounds exception using a smaller subset of numbers to index my list.
RecyclerView does not rebind views when their positions change (for obvious performance reasons).
For example, if your data set looks like this:
A B C D
and you add item X via
mItems.add(1, X);
notifyItemInserted(1, 1);
to get
A X B C D
RecyclerView will only bind X and run the animation.
There is a getPosition method in ViewHolder but that may not match adapter position if you call it in the middle of an animation.
If you need the adapter position, your safest option is getting the position from the Adapter.
update for your comment
Add a Task field to the ViewHolder.
Change onCreateViewHolder as follows to avoid creating a listener object on each rebind.
// inflates row to create a viewHolder
#Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int type) {
View itemView = LayoutInflater.from(parent.getContext()).
inflate(R.layout.list_item, parent, false);
final TaskViewHolder vh = new TaskViewHolder(itemView);
taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// delete from SQLite DB
Task taskToDel = vh.getTask();
final int pos = taskList.indexOf(taskToDel);
if (pos == -1) return;
taskToDel.delete();
// updating UI
taskList.remove(pos);
thisAdapter.notifyItemRemoved(pos);
}
});
}
so in your on bind method, you do
// onBindViewHolder binds a model to a viewholder
#Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
Task currTask = taskList.get(pos);
taskViewHolder.setTask(currTask);
taskViewHolder.taskTV.setText(currTask.getDescription());
}
Like yigit said, RecyclerView works like that:
A B C D
and you add item X via
mItems.add(1, X);
notifyItemInserted(1, 1);
you get
A X B C D
Using holder.getAdapterPosition() in onClickListener() will give you the right item from dataset to be removed, not the "static" view position. Here's the doc about it onBindViewHolder
Why dont you use a public interface for the button click and controle the action in the MainActivity.
In your adapter add:
public interface OnItemClickListener {
void onItemClick(View view, int position, List<Task> mTaskList);
}
and
public OnItemClickListener mItemClickListener;
// Provide a suitable constructor (depends on the kind of dataset)
public TaskAdapter (List<Task> myDataset, OnItemClickListener mItemClickListener) {
this.mItemClickListener = mItemClickListener;
this.mDataset = mDataset;
}
plus the call in the ViewHolder class
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public ViewHolder(View v) {
super(v);
...
closeBtn = (ImageView)v.findViewById(R.id.xImg);
closeBtn.setOnClickListener(this);
}
#Override
public void onClick(View v) {
// If not long clicked, pass last variable as false.
mItemClickListener.onItemClick(v, getAdapterPosition(), mDataset);
}
}
In your MainActivity change your adapter to handle the call
// set Adapter
mAdapter = new TaskAdapter(taskList, new TaskAdapter.OnItemClickListener() {
#Override
public void onItemClick(View v, int position) {
if (v.getId() == R.id.xImg) {
Task taskToDel = taskList.get(position);
// updating UI
taskList.remove(position);
thisAdapter.notifyItemRemoved(position);
// remove from db with unique id to use delete query
// dont use the position but something like taskToDel.getId()
taskToDel.delete();
}
}
});
Thanks to #yigit for his answer, his solution mainly worked, I just tweaked it a little bit so as to avoid using vh.getTask() which I was not sure how to implement.
final ViewHolder vh = new ViewHolder(customView);
final KittyAdapter final_copy_of_this = this;
// We attach a CheckChange Listener here instead of onBindViewHolder
// to avoid creating a listener object on each rebind
// Note Rebind is only called if animation must be called on view (for efficiency)
// It does not call on the removed if the last item is checked
vh.done.setChecked(false);
vh.done.setOnCheckedChangeListener(null);
vh.done.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
buttonView.setEnabled(false);
final int pos2 = vh.getAdapterPosition(); // THIS IS HOW TO GET THE UPDATED POSITION
// YOU MUST UPDATE THE DATABASE, removed by Title
DatabaseHandler db = new DatabaseHandler(mContext);
db.remove(mDataSet.get(pos2).getTitle(), fp);
db.close();
// Update UI
mDataSet.remove(pos2);
final_copy_of_this.notifyItemRemoved(pos2);
}
});
Notice instead to get the updated position, you can call vh.getAdapterPosition(), which is the line that will give you the updated position from the underlying dataset rather than the fake view.
This is working for me as of now, if someone knows of a drawback to using this please let me know. Hope this helps someone.
Personally, I don't like this concept of RecyclerViews. Seems like it wasn't thought of completely.
As it was said when removing an item the Recycler view just hides an item. But usually you don't want to leave that item in your collection. When deleting an item from the collection "it shifts its elements towards 0" whereas recyclerView keeps the same size.
If you are calling taskList.remove(position); your position must be evaluated again:
int position = recyclerView.getChildAdapterPosition(taskViewHolder.itemView);

Categories

Resources