20

I have some data in SQLite database.I have a content provider which will get the data from the database. Now the problem is how do i implement cursorLoader to work in hand with recyclerview?

Also, anyone can explain why we need to transfer the data from cursor to an object to display in a listview/recyclerview and not straight from a cursor?

For example, In the custom cursorAdapter class,

Person person = new Person(cursor.getString(cursor.getColumnIndexOrThrow(PERSON_NAME)));
textview.setText = person.getName();

OR

textview.setText = cursor.getString(cursor.getColumnIndexOrThrow(PERSON_NAME));

Which one of the above method is better?

In the past, we used to have listview and gridview and it seems now they are combined to become recyclerview. Then, how do i implement a grid based recyclerview?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Abdul Rahman
  • 1,833
  • 1
  • 21
  • 21
  • you dont have to use any list: use ItemBridgeAdapter + CursorObjectAdapter or bind the Cursor directly in your custom RecyclerView.Adapter – pskink Jan 01 '15 at 12:58
  • honestly i dont know why CursorObjectAdapter is in "leanback" support library which is designed for TV devices, imho it should be part of any "general purposes" support library – pskink Jan 01 '15 at 14:21
  • @pskink I am having to solve this problem presently. Will you please provide a short example of how I could `bind the Cursor directly in [my] custom RecyclerView.Adapter`? That would be tremendously helpful. Thanks. And please tag me in the comment of the response when you do so I know. Thanks for any help you can provide. – Katedral Pillon Sep 05 '15 at 04:57
  • 1
    @KatedralPillon use this for example: https://gist.github.com/Shywim/127f207e7248fe48400b – pskink Sep 05 '15 at 05:12
  • @pskink Thanks. Is it more efficient than loading the cursor into an ArrayList and then passing the ArrayList to the RecyclerView? – Katedral Pillon Sep 05 '15 at 05:48
  • 1
    @KatedralPillon of course! never load the cursor into any list, it seems that 99% folks here do such a stupid thing i don't know why (they need a custom adapter (for a `ListView` it's a custom `BaseAdapter` / `ArrayAdapter`) / they need a POJO to keep data / they need a loop to iterate over the cursor) why??? i don't know – pskink Sep 05 '15 at 05:58
  • @pskink I am wondering if you have a more detailed example of how to set the cursor directly in a custom recyclerview. The link you posted alone is kind of confusing to a noob. – Micro Sep 11 '15 at 20:01
  • @MicroR extend `CursorRecyclerAdapter` and implement its `onBindViewHolderCursor` method, that's all – pskink Sep 11 '15 at 20:16

4 Answers4

4

In general you should try to separate view and data responsibilities. So what you need is to get all your objects from the database in advance then set up an Adapter which looks like the following:

public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
    private final List<Person> objectList = new ArrayList<>();

    @Override
    public CustomAdapter.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
        final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
        return new ViewHolder(layoutInflater.inflate(R.layout.adapter_item, parent, false));
    }

    @Override
    public void onBindViewHolder(final CustomAdapter.ViewHolder holder, final int position) {
        holder.bindItem(objectList.get(position));
    }

    // Set the persons for your adapter
    public void setItems(final List<Person> persons) {
        objectList.addAll(persons);
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        return objectList.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private final TextView mTextViewTitle;
        private Object mObject;

        public ViewHolder(final View itemView) {
            super(itemView);
            mTextViewTitle = (TextView) itemView.findViewById(R.id.view_item_textViewTitle);                
            mTextViewTitle.setText(mObject.getText());
        }

        private void bindItem(@NonNull final Person object) {
            mObject = object;
        }
    }
}

Then you can bind the adapter to the RecyclerView by:

final CustomAdapter adapter = new CustomAdapter();
adapter.setItems(mPersons);
mRecyclerView.setAdapter();

To answer your second question ("In the past, we used to have listview and gridview and it seems now they are combined to become recyclerview. Then, how do i implement a grid based recyclerview?"):

When binding a LayoutManager to the RecyclerView you can decide which one you take:

final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(layoutManager);

or

final GridLayoutManager layoutManager = new GridLayoutManager(this, COLUMN_SPAN_COUNT);
mRecyclerView.setLayoutManager(layoutManager);

There are several LayoutManagers. Find out more here.

UPDATE: You don't have to load all items in advance, just rename setItems to addItems and you are good to go

luckyhandler
  • 10,651
  • 3
  • 47
  • 64
  • 1
    You say:"So what you need is to get all your objects from the database in advance", but what if I have 12.000 object to fetch from database, and this is the main reason choose ContentProvider mechanism (to do lazy loading for me) ? – miroslavign Jan 18 '16 at 15:15
  • How are you suppoused to keep the array in sync with the database? – 0xcaff Jan 12 '17 at 04:00
3

There are several Github gists/projects like this and this which show such a use case.

While you would be using a Adapter which is custom-tailored to a cursor adapter,you would use a GridLayoutManager/LinearLayoutManager as usual for the same.

srv_sud
  • 647
  • 1
  • 9
  • 26
Droidekas
  • 3,464
  • 2
  • 26
  • 40
1

I think you can use directly Custom CursorAdapter for RecyclerView so you do not have to convert Cursor to ArrayList:

public class ProductListAdapter extends RecyclerView.Adapter<ProductListAdapter.ViewHolder> {

    // Because RecyclerView.Adapter in its current form doesn't natively 
    // support cursors, we wrap a CursorAdapter that will do all the job
    // for us.
    CursorAdapter mCursorAdapter;

    Activity mContext;
    Random rnd;

    public ProductListAdapter(AppCompatActivity context, Cursor c) {

        mContext = context;
        rnd = new Random();

        mCursorAdapter = new CursorAdapter(mContext, c, 0) {

            @Override
            public View newView(Context context, Cursor cursor, ViewGroup parent) {
                // Inflate the view here
                LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
                return inflater.inflate(R.layout.row_product_layout_grid, parent, false);
            }

            @Override
            public void bindView(View view, Context context, Cursor cursor) {
                String productName = cursor.getString(cursor.getColumnIndex(TProduct.PRODUCT_NAME));

                // Binding operations
                ((TextView) view.findViewById(R.id.sub_product_name_text_view)).setText(productName);



                int color = Color.argb(200, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));

                String url = "http://dummyimage.com/300/" + color + "/ffffff&text=" + (cursor.getPosition() + 1);

                Picasso
                        .with(context)
                        .load(url)
                        .placeholder(R.mipmap.ic_launcher) // can also be a drawable
                        .into((ImageView) view.findViewById(R.id.sub_product_image_view));
            }
        };
    }

    public void reQuery(Cursor c) {
        if (mCursorAdapter != null) {
            mCursorAdapter.changeCursor(c);
            mCursorAdapter.notifyDataSetChanged();
        }
    }

    @Override
    public int getItemCount() {
        return mCursorAdapter.getCount();
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // Passing the binding operation to cursor loader
        mCursorAdapter.getCursor().moveToPosition(position); //EDITED: added this line as suggested in the comments below, thanks :)
        mCursorAdapter.bindView(holder.view, mContext, mCursorAdapter.getCursor());
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Passing the inflater job to the cursor-adapter
        View v = mCursorAdapter.newView(mContext, mCursorAdapter.getCursor(), parent);
        return new ViewHolder(v);
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        View view;
        public ViewHolder(View itemView) {
            super(itemView);
            view = itemView.findViewById(R.id.product_row_card_view);
        }
    }
}

May it will be useful to you. :)

Pratik Butani
  • 60,504
  • 58
  • 273
  • 437
  • https://stackoverflow.com/questions/39825125/android-recyclerview-cursorloader-contentprovider-load-more – zhirzh Oct 04 '20 at 19:37
0

Uses a RecyclerView to display a list of contacts fetched from a ContentProvider using a CursorLoader.

1. ShowContactBook Class where show recycler view

public class ShowContactBook extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{

private static final String TAG = MainActivity.class.getSimpleName();
private static final int TASK_LOADER_ID = 0;

private CustomCursorAdapter mAdapter;
RecyclerView mRecyclerView;

Toolbar toolbar;

CollapsingToolbarLayout collapsingToolbarLayout;

TextView textViewTotalContacts;

SearchView searchView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_show_contact_book);

    collapsingToolbarLayout = findViewById(R.id.collapsing_toolbar);
    toolbar = findViewById(R.id.toolbar);
    textViewTotalContacts = findViewById(R.id.total_contacts);
    searchView=findViewById(R.id.search);


    setSupportActionBar(toolbar);

    int color = ContextCompat.getColor(this, R.color.black);
    Drawable icon = AppCompatResources.getDrawable(this, R.drawable.arrow_back_24);
    if (icon != null) {
        icon = DrawableCompat.wrap(icon);
        DrawableCompat.setTint(icon, color);
        toolbar.setNavigationIcon(icon);
    }

    toolbar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            onBackPressed();
        }
    });

    collapsingToolbarLayout.setTitle("Contacts");


    mRecyclerView = findViewById(R.id.list);

    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

    mAdapter = new CustomCursorAdapter(this);
    mRecyclerView.setAdapter(mAdapter);
    
    /*
     Add a touch helper to the RecyclerView to 
     recognize when a user swipes to delete an 
     item. An ItemTouchHelper enables touch 
     behavior (like swipe and move) on each 
     ViewHolder,and uses callbacks to signal when 
     a user is performing these actions.
     */

    new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
            return false;
        }

        //* Called when a user swipes left or right on a ViewHolder*/

        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            //* Here is where you'll implement swipe to delete*/

            int id = (int) viewHolder.itemView.getTag();

            String stringId = Integer.toString(id);
            Uri uri = ContactBookContract.contactEntry.CONTENT_URI;
            uri = uri.buildUpon().appendPath(stringId).build();

            /* or
            // Build the URI directly without converting the id to a string
            Uri uri = ContentUris.withAppendedId(TaskContract.TaskEntry.CONTENT_URI, id);
            */


            getContentResolver().delete(uri, null, null);


            getSupportLoaderManager().restartLoader(TASK_LOADER_ID, null, ShowContactBook.this);
        }
    }).attachToRecyclerView(mRecyclerView);



    mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            showItemCount();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            showItemCount();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            showItemCount();
        }
    });


    searchView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            searchView.setIconified(false);
            Toast.makeText(ShowContactBook.this, "Searching", Toast.LENGTH_SHORT).show();
        }
    });


    /*
     Ensure a loader is initialized and active. If the loader doesn't already exist, one is
     created, otherwise the last created loader is re-used.
     */
    getSupportLoaderManager().initLoader(TASK_LOADER_ID, null, this);

}

private void showItemCount() {

    int count = mAdapter.getItemCount();
    String message = count + " Contacts";
    textViewTotalContacts.setText(message);
}

@Override
protected void onResume() {
    super.onResume();
    getSupportLoaderManager().restartLoader(TASK_LOADER_ID, null, ShowContactBook.this);
}

@SuppressLint("StaticFieldLeak")
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
    return new AsyncTaskLoader<Cursor>(this) {

        Cursor mTaskData = null;

        @Override
        protected void onStartLoading() {
            if (mTaskData != null) {

                deliverResult(mTaskData);
            } else {
                forceLoad();
            }
        }


        @Override
        public Cursor loadInBackground() {

            try {
                return getContentResolver().query(ContactBookContract.contactEntry.CONTENT_URI,
                        null,
                        null,
                        null,
                        ContactBookContract.contactEntry._ID + " DESC");

            } catch (Exception e) {
                Log.e(TAG, "Failed to asynchronously load data.");
                e.printStackTrace();
                return null;
            }
        }

        public void deliverResult(Cursor data) {
            mTaskData = data;
            super.deliverResult(data);
        }
    };
}

/**
 * Called when a previously created loader has finished its load.
 *
 * @param loader The Loader that has finished.
 * @param data   The data generated by the Loader.
 */

@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
    // Update the data that the adapter uses to create ViewHolders
    mAdapter.swapCursor(data);
}

/**
 * Called when a previously created loader is being reset, and thus
 * making its data unavailable.
 * onLoaderReset removes any references this activity had to the loader's data.
 *
 * @param loader The Loader that is being reset.
 */

@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
}

@Override
public void onBackPressed() {
    super.onBackPressed();
}

}

1. CustomCursorAdapter Class : The CustomCursorAdapter class is a custom implementation of the RecyclerView.Adapter used to display data from a Cursor in a RecyclerView. In the context of the provided code, this adapter is used to display a list of contacts fetched from a ContentProvider.

public class CustomCursorAdapter extends RecyclerView.Adapter<CustomCursorAdapter.TaskViewHolder> {
private Cursor cursor;
private Context mContext;

/**
 * Constructor for the CustomCursorAdapter that initializes the Context.
 *
 * @param mContext the current Context
 */

public CustomCursorAdapter(Context mContext) {
    this.mContext = mContext;
}


/**
 * Called when ViewHolders are created to fill a RecyclerView.
 *
 * @return A new TaskViewHolder that holds the view for each task
 */

@Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    View view = LayoutInflater.from(mContext)
            .inflate(R.layout.list_view, parent, false);

    return new TaskViewHolder(view);
}


/**
 * Called by the RecyclerView to display data at a specified position in the Cursor.
 *
 * @param holder   The ViewHolder to bind Cursor data to
 * @param position The position of the data in the Cursor
 */

@Override
public void onBindViewHolder(TaskViewHolder holder, int position) {

    int idIndex = cursor.getColumnIndex(ContactBookContract.contactEntry._ID);
    int nameColumnIndex = cursor.getColumnIndex(ContactBookContract.contactEntry.COLUMN_CONTACT_NAME);
    int phColumnIndex = cursor.getColumnIndex(ContactBookContract.contactEntry.COLUMN_CONTACT_PH);
    int emailColumnIndex = cursor.getColumnIndex(ContactBookContract.contactEntry.COLUMN_CONTACT_EMAIL);
    int imageColumnIndex = cursor.getColumnIndex(ContactBookContract.contactEntry.COLUMN_CONTACT_IMAGE);

    cursor.moveToPosition(position); 

    final int id = cursor.getInt(idIndex);
    String name = cursor.getString(nameColumnIndex);
    String ph = cursor.getString(phColumnIndex);
    String email = cursor.getString(emailColumnIndex);

    //* Extract the image byte array from the cursor*/
    byte[] imageByteArray = cursor.getBlob(imageColumnIndex);

    //* Convert byte array back to Bitmap*/
    Bitmap bitmap = BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.length);

    holder.itemView.setTag(id);
    holder.nameTextView.setText(name);
    holder.circleImageView.setImageBitmap(bitmap);


    holder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            int id = (int) holder.itemView.getTag();
            Uri uri = ContentUris.withAppendedId(ContactBookContract.contactEntry.CONTENT_URI, id);
          
            Toast.makeText(mContext, "" + uri.toString() + " : " + name, Toast.LENGTH_SHORT).show();
            Intent intent = new Intent(mContext, ContactDetail.class);
           
            intent.setData(uri);
            mContext.startActivity(intent);

            applyTemporaryHoverEffect(holder.itemView);
        }
    });
}

private void applyTemporaryHoverEffect(View view) {
    final int originalBackgroundColor = view.getSolidColor();
    final int hoverColor = ContextCompat.getColor(mContext, R.color.hoverColor);
    final int duration = 100; 
    view.setBackgroundColor(hoverColor);
    
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            view.setBackgroundColor(originalBackgroundColor);
        }
    }, duration);
}

/**
 * Returns the number of items to display.
 */

@Override
public int getItemCount() {
    if (cursor == null) {
        return 0;
    }
    return cursor.getCount();
}


/**
 * When data changes and a re-query occurs, this function swaps the old Cursor
 * with a newly updated Cursor (Cursor c) that is passed in.
 */

public Cursor swapCursor(Cursor c) {
    // check if this cursor is the same as the previous cursor (mCursor)
    if (cursor == c) {
        return null;
    }
    Cursor temp = cursor;
    this.cursor = c;

    if (c != null) {
        this.notifyDataSetChanged();
    }
    return temp;
}

//* Inner class for creating ViewHolders*/
class TaskViewHolder extends RecyclerView.ViewHolder {


    TextView nameTextView;
    CircleImageView circleImageView;

    /**
     * Constructor for the TaskViewHolders.
     *
     * @param itemView The view inflated in onCreateViewHolder
     */

    public TaskViewHolder(View itemView) {
        super(itemView);
        nameTextView = (TextView) itemView.findViewById(R.id.name);
        circleImageView = (CircleImageView) itemView.findViewById(R.id.list_imageView);
    }
}

}