Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android RecyclerView + CursorLoader + ContentProvider + "Load More"

I have created one Activity in that I am implementing CursorLoader for load data from Database.

I have done that thing for all records of that Table but I want to load 30-30 records like Load More Functionality

I have tried to create query and its loading first 30 records but I cant able to understand how can I request for new records.

My Activity Code is Like:

public class ProductListActivity extends AppCompatActivity         implements LoaderManager.LoaderCallbacks<Cursor> {      /**      * Records in list      */     int offset = 0;      /**      * For Current Activity *      */     Context mContext;      /**      * Activity Binding      */     ActivityProductListBinding activityProductListBinding;      /**      * Product Adapter      */     ProductListAdapter productListAdapter;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);          /**          * DataBinding with XML          */         activityProductListBinding = DataBindingUtil.setContentView(this, R.layout.activity_product_list);          /**          * Getting Context          */         mContext = getApplicationContext();          /***          * TOOLBAR Settings...          */         setSupportActionBar(activityProductListBinding.toolbar);         activityProductListBinding.toolbarTitleTextview.setText(R.string.string_title_products);         getSupportActionBar().setDisplayHomeAsUpEnabled(true);         getSupportActionBar().setDisplayShowTitleEnabled(false);          final ActionBar ab = getSupportActionBar();         if (ab != null)             ab.setDisplayHomeAsUpEnabled(true);          /**          * RecyclerView Setup          */         GridLayoutManager manager = new GridLayoutManager(this, 2);         activityProductListBinding.productListRecyclerView.setLayoutManager(manager);          /**          * First Time init Loader          */         getSupportLoaderManager().initLoader(1, null, this);     }      @Override     public Loader<Cursor> onCreateLoader(int id, Bundle args) {         final Uri CONTENT_URI = KOOPSContentProvider.CONTENT_URI_PRODUCT.buildUpon()                 .appendQueryParameter(KOOPSContentProvider.QUERY_PARAMETER_OFFSET,                         String.valueOf(offset))                 .build();         return new CursorLoader(this, CONTENT_URI ,null, null, null, null);     }      @Override     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {         //When the loader has loaded some data (either initially, or the         //datasource has changed and a new cursor is being provided),         //Then we'll swap out the cursor in our recyclerview's adapter         // and we'll create the adapter if necessary         Log.d(LogUtils.TAG, "Cursor : " + data.getCount());         if (productListAdapter == null) {             productListAdapter = new ProductListAdapter(this, data);             activityProductListBinding.productListRecyclerView.setAdapter(productListAdapter);         }     }      @Override     public void onLoaderReset(Loader<Cursor> loader) {         //If the loader is reset, we need to clear out the         //current cursor from the adapter.         productListAdapter.reQuery(null);     } } 

UPDATE:

I have added EndlessRecyclerViewScrollListener

activityProductListBinding.productListRecyclerView.addOnScrollListener(new EndlessRecyclerViewScrollListener(manager) {             @Override             public void onLoadMore(int page, int totalItemsCount) {                 // Triggered only when new data needs to be appended to the list                 // Add whatever code is needed to append new items to the bottom of the list                 offset = limit * page;                 /**                  * Adding Bundle in Loader and then Call                  */                 getSupportLoaderManager().initLoader(LOADER_ID, productQueryData, ProductListActivity.this);             }         }); 

I have tried to MergeCursor in adapter but getting error:

FATAL EXCEPTION: main  Process: com.kevalam.koopsv3, PID: 25021  java.lang.IllegalStateException: Observer android.database.MergeCursor$1@570f82d is already registered.     at android.database.Observable.registerObserver(Observable.java:49)     at android.database.AbstractCursor.registerDataSetObserver(AbstractCursor.java:358)     at android.database.CursorWrapper.registerDataSetObserver(CursorWrapper.java:222)     at android.database.MergeCursor.<init>(MergeCursor.java:50)     at com.kevalam.koops.adapter.ProductListAdapter.mergeCursor(ProductListAdapter.java:71)     at com.kevalam.koops.ui.ProductListActivity.onLoadFinished(ProductListActivity.java:161)     at com.kevalam.koops.ui.ProductListActivity.onLoadFinished(ProductListActivity.java:38) 

Edited (ADAPTER Code):

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(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 mergeCursor(Cursor c) {         if (mCursorAdapter != null) {             Cursor[] cursorArray = {mCursorAdapter.getCursor(), c};             MergeCursor mergeCursor = new MergeCursor(cursorArray);             reQuery(mergeCursor);         }     }      public void reQuery(Cursor c) {         if (mCursorAdapter != null) {             mCursorAdapter.changeCursor(c);             mCursorAdapter.notifyDataSetChanged();             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);         }     } } 

Anybody can help please, Thanks in advance.

like image 422
Pratik Butani Avatar asked Oct 03 '16 05:10

Pratik Butani


People also ask

How implement load more RecyclerView in Android?

How is this implemented? Typically in a simple RecyclerView, we load elements to the adapter from a Data Structure. In order to show the loading icon view at the bottom of the RecyclerView, we need to first add a NULL element to the end of the Data Structure.

Is there an addHeaderView equivalent for RecyclerView?

addHeaderView() but you can achieve this by adding a type to your adapter for header. everything seems to be OK and it should work, but however make the recycler view MATCH_PARENT see if anything changes. also if its possible make the recycler view the root of that layout (its good for performance).

Is RecyclerView deprecated?

Today, suddenly Recyclerview. Viewholder became deprecated. Other, android project is no deprecated.

What is viewType in onCreateViewHolder?

This viewType variable is internal to the Adapter class. It's used in the onCreateViewHolder() and onBindViewHolder to inflate and populate the mapped layouts. Before we jump into the implementation of the Adapter class, let's look at the types of layouts that are defined for each view type.


1 Answers

Update 01.10.2017 Google announced new library for paging, more info here https://developer.android.com/topic/libraries/architecture/paging.html


Here is working sample with pagination based on cursoradapter+recyclerview+provider.

I give you step by step with code + bonus with gif preview.

But IMHO pagination on cursor adapter has nonsense because db is handling all heavy stuff with load more data :)

Step 1. create database:

public class CustomSqliteOpenHelper extends SQLiteOpenHelper {      private static final String TAG = "CustomSqliteOpenHelper";      public CustomSqliteOpenHelper(Context context) {         super(context, "db.db", null, 1);     }      @Override     public void onOpen(SQLiteDatabase db) {         super.onOpen(db);     }      @Override     public void onCreate(SQLiteDatabase db) {         db.execSQL(TableItems.CREATE_TABLE);     }      @Override     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {         db.execSQL(TableItems.DROP_TABLE);         onCreate(db);     }  } 

Step 2. create table

public class TableItems {     public static final String NAME = TableItems.class.getSimpleName().toLowerCase();     public static final String _ID = "_id";     public static final String TEXT = "text";      public static final String CREATE_TABLE =             "CREATE TABLE " + NAME +                     " ( " +                     _ID + " integer primary key autoincrement, " +                     TEXT + " text " +                     " ); ";      public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + NAME;     public static String[] Columns = new String[]{_ID, TEXT}; } 

Step 3. create provider

import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.support.annotation.NonNull; import android.util.Log;  import com.example.pagingproject.BuildConfig;  public class RequestProvider extends ContentProvider {     private static final String TAG = "RequestProvider";      private SQLiteOpenHelper mSqliteOpenHelper;     private static final UriMatcher sUriMatcher;      public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".db";      private static final int             TABLE_ITEMS = 0;      static {         sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);         sUriMatcher.addURI(AUTHORITY, TableItems.NAME + "/offset/" + "#", TABLE_ITEMS);     }      public static Uri urlForItems(int limit) {         return Uri.parse("content://" + AUTHORITY + "/" + TableItems.NAME + "/offset/" + limit);     }      @Override     public boolean onCreate() {         mSqliteOpenHelper = new CustomSqliteOpenHelper(getContext());         return true;     }      @Override     synchronized public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {          SQLiteDatabase db = mSqliteOpenHelper.getReadableDatabase();         SQLiteQueryBuilder sqb = new SQLiteQueryBuilder();         Cursor c = null;         String offset;          switch (sUriMatcher.match(uri)) {             case TABLE_ITEMS: {                 sqb.setTables(TableItems.NAME);                 offset = uri.getLastPathSegment();                 break;             }              default:                 throw new IllegalArgumentException("uri not recognized!");         }          int intOffset = Integer.parseInt(offset);          String limitArg = intOffset + ", " + 30;         Log.d(TAG, "query: " + limitArg);         c = sqb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limitArg);          c.setNotificationUri(getContext().getContentResolver(), uri);          return c;     }      @Override     public String getType(@NonNull Uri uri) {         return BuildConfig.APPLICATION_ID + ".item";     }      @Override     public Uri insert(@NonNull Uri uri, ContentValues values) {         String table = "";         switch (sUriMatcher.match(uri)) {             case TABLE_ITEMS: {                 table = TableItems.NAME;                 break;             }         }          long result = mSqliteOpenHelper.getWritableDatabase().insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);          if (result == -1) {             throw new SQLException("insert with conflict!");         }          Uri retUri = ContentUris.withAppendedId(uri, result);         return retUri;     }      @Override     public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {         return -1;     }      @Override     public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {         return -1;     } } 

Step 4. create an abstract cursor adapters, I took sample from StackOverflow custom-cursor-recyclerView-adapter

import android.content.Context; import android.database.Cursor; import android.database.DataSetObserver; import android.support.v7.widget.RecyclerView; import android.view.ViewGroup;  /**  * Created by skyfishjy on 10/31/14.  */  public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {      protected Context mContext;      private Cursor mCursor;      private boolean mDataValid;      private int mRowIdColumn;      private DataSetObserver mDataSetObserver;      public CursorRecyclerViewAdapter(Context context, Cursor cursor) {         mContext = context;         mCursor = cursor;         mDataValid = cursor != null;         mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;         mDataSetObserver = new NotifyingDataSetObserver(this);         if (mCursor != null) {             mCursor.registerDataSetObserver(mDataSetObserver);         }     }      public Cursor getCursor() {         return mCursor;     }      @Override     public int getItemCount() {         if (mDataValid && mCursor != null) {             return mCursor.getCount();         }         return 0;     }      @Override     public long getItemId(int position) {         if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {             return mCursor.getLong(mRowIdColumn);         }         return 0;     }      @Override     public void setHasStableIds(boolean hasStableIds) {         super.setHasStableIds(true);     }      public static final String TAG = CursorRecyclerViewAdapter.class.getSimpleName();      public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);      @Override     public VH onCreateViewHolder(ViewGroup parent, int viewType) {         return null;     }      @Override     public void onBindViewHolder(VH viewHolder, int position) {         if (!mDataValid) {             throw new IllegalStateException("this should only be called when the cursor is valid");         }         if (!mCursor.moveToPosition(position)) {             throw new IllegalStateException("couldn't move cursor to position " + position);         }         onBindViewHolder(viewHolder, mCursor);     }      /**      * Change the underlying cursor to a new cursor. If there is an existing cursor it will be      * closed.      */     public void changeCursor(Cursor cursor) {         Cursor old = swapCursor(cursor);         if (old != null) {             old.close();         }     }      /**      * Swap in a new Cursor, returning the old Cursor.  Unlike      * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>      * closed.      */     public Cursor swapCursor(Cursor newCursor) {         if (newCursor == mCursor) {             return null;         }         final Cursor oldCursor = mCursor;         if (oldCursor != null && mDataSetObserver != null) {             oldCursor.unregisterDataSetObserver(mDataSetObserver);         }         mCursor = newCursor;         if (mCursor != null) {             if (mDataSetObserver != null) {                 mCursor.registerDataSetObserver(mDataSetObserver);             }             mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");             mDataValid = true;             notifyDataSetChanged();         } else {             mRowIdColumn = -1;             mDataValid = false;             notifyDataSetChanged();             //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter         }         return oldCursor;     }      public void setDataValid(boolean mDataValid) {         this.mDataValid = mDataValid;     }      private class NotifyingDataSetObserver extends DataSetObserver {         private RecyclerView.Adapter adapter;          public NotifyingDataSetObserver(RecyclerView.Adapter adapter) {             this.adapter = adapter;         }          @Override         public void onChanged() {             super.onChanged();             ((CursorRecyclerViewAdapter) adapter).setDataValid(true);             adapter.notifyDataSetChanged();         }          @Override         public void onInvalidated() {             super.onInvalidated();             ((CursorRecyclerViewAdapter) adapter).setDataValid(false);         }     } } 

Step 5. create your own adapter with extending(inheriting) previous class

import android.content.Context; import android.database.Cursor; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup;  /**  * Created by deadfish on 2016-01-28.  */ public class CustomCursorRecyclerViewAdapter extends CursorRecyclerViewAdapter {      public CustomCursorRecyclerViewAdapter(Context context, Cursor cursor) {         super(context, cursor);     }      @Override     public long getItemId(int position) {         return super.getItemId(position);     }      @Override     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {         View v = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false);         return new CustomViewHolder(v);     }      @Override     public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, Cursor cursor) {         CustomViewHolder holder = (CustomViewHolder) viewHolder;         cursor.moveToPosition(cursor.getPosition());         holder.setData(cursor);     }      @Override     public int getItemCount() {         return super.getItemCount();     }      @Override     public int getItemViewType(int position) {         return 0;     } } 

Step 6. create custom viewHolder

import android.database.Cursor; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.TextView;  public class CustomViewHolder extends RecyclerView.ViewHolder {      public TextView textView1;      public CustomViewHolder(View itemView) {         super(itemView);         textView1 = (TextView) itemView.findViewById(android.R.id.text1);     }      public void setData(Cursor c) {         textView1.setText(c.getString(c.getColumnIndex("text")));     } } 

Step 7. write code in sample MainActivity

import android.content.ContentValues; import android.database.Cursor; import android.database.MatrixCursor; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.widget.Toast;  import com.example.pagingproject.adapters.CustomCursorRecyclerViewAdapter; import com.example.pagingproject.databases.RequestProvider; import com.example.pagingproject.databases.TableItems;  public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {      public final int offset = 30;     private int page = 0;      private RecyclerView mRecyclerView;     private boolean loadingMore = false;     private Toast shortToast;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);         CustomCursorRecyclerViewAdapter mAdapter = new CustomCursorRecyclerViewAdapter(this, null);          mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);         mRecyclerView.setLayoutManager(mLayoutManager);         mRecyclerView.setAdapter(mAdapter);          int itemsCountLocal = getItemsCountLocal();         if (itemsCountLocal == 0) {             fillTestElements();         }          shortToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);          mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {             @Override             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {                 super.onScrolled(recyclerView, dx, dy);                  LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();                 int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();                 int maxPositions = layoutManager.getItemCount();                  if (lastVisibleItemPosition == maxPositions - 1) {                     if (loadingMore)                         return;                      loadingMore = true;                     page++;                     getSupportLoaderManager().restartLoader(0, null, MainActivity.this);                 }             }         });          getSupportLoaderManager().restartLoader(0, null, this);     }      private void fillTestElements() {         int size = 1000;         ContentValues[] cvArray = new ContentValues[size];         for (int i = 0; i < cvArray.length; i++) {             ContentValues cv = new ContentValues();             cv.put(TableItems.TEXT, ("text " + i));             cvArray[i] = cv;         }          getContentResolver().bulkInsert(RequestProvider.urlForItems(0), cvArray);     }      private int getItemsCountLocal() {         int itemsCount = 0;          Cursor query = getContentResolver().query(RequestProvider.urlForItems(0), null, null, null, null);         if (query != null) {             itemsCount = query.getCount();             query.close();         }         return itemsCount;     }      /*loader*/      @Override     public Loader<Cursor> onCreateLoader(int id, Bundle args) {         switch (id) {             case 0:                 return new CursorLoader(this, RequestProvider.urlForItems(offset * page), null, null, null, null);             default:                 throw new IllegalArgumentException("no id handled!");         }     }      @Override     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {         switch (loader.getId()) {             case 0:                 Log.d(TAG, "onLoadFinished: loading MORE");                 shortToast.setText("loading MORE " + page);                 shortToast.show();                  Cursor cursor = ((CustomCursorRecyclerViewAdapter) mRecyclerView.getAdapter()).getCursor();                  //fill all exisitng in adapter                 MatrixCursor mx = new MatrixCursor(TableItems.Columns);                 fillMx(cursor, mx);                  //fill with additional result                 fillMx(data, mx);                  ((CustomCursorRecyclerViewAdapter) mRecyclerView.getAdapter()).swapCursor(mx);                   handlerToWait.postDelayed(new Runnable() {                     @Override                     public void run() {                         loadingMore = false;                     }                 }, 2000);                  break;             default:                 throw new IllegalArgumentException("no loader id handled!");         }     }      private Handler handlerToWait = new Handler();      private void fillMx(Cursor data, MatrixCursor mx) {         if (data == null)             return;          data.moveToPosition(-1);         while (data.moveToNext()) {             mx.addRow(new Object[]{                     data.getString(data.getColumnIndex(TableItems._ID)),                     data.getString(data.getColumnIndex(TableItems.TEXT))             });         }     }      @Override     public void onLoaderReset(Loader<Cursor> loader) {         // TODO: 2016-10-13     }      //      private static final String TAG = "MainActivity";  } 

Step 8. declare provider in AndroidManifest

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     package="com.example.pagingproject">      <application         android:allowBackup="true"         android:icon="@mipmap/ic_launcher"         android:label="@string/app_name"         android:supportsRtl="true"         android:theme="@style/AppTheme">         <activity android:name=".MainActivity">             <intent-filter>                 <action android:name="android.intent.action.MAIN" />                  <category android:name="android.intent.category.LAUNCHER" />             </intent-filter>         </activity>          <provider             android:name=".databases.RequestProvider"             android:authorities="${applicationId}.db"             android:exported="false" />     </application>  </manifest> 

Step 9. create xml for MainActivity class

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:id="@+id/activity_main"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:paddingBottom="@dimen/activity_vertical_margin"     android:paddingLeft="@dimen/activity_horizontal_margin"     android:paddingRight="@dimen/activity_horizontal_margin"     android:paddingTop="@dimen/activity_vertical_margin"     tools:context="com.example.pagingproject.MainActivity">      <android.support.v7.widget.RecyclerView         android:id="@+id/recyclerView"         android:layout_width="match_parent"         android:layout_height="match_parent" /> </RelativeLayout> 

Test it:

Trigger to load more is every 30th item element, so if index starts from 0, 29 will be the trigger.

enter image description here

like image 156
deadfish Avatar answered Oct 08 '22 18:10

deadfish