Join FlipAndroid.COM Telegram Group: https://t.me/joinchat/F_aqThGkhwcLzmI49vKAiw


Android RecyclerView + CursorLoader + ContentProvider + "Cargar más"

He creado una actividad en la que estoy implementando CursorLoader para cargar datos desde la base de datos.

He hecho esa cosa para todos los registros de esa tabla, pero quiero cargar 30-30 registros como Load More Functionality

He tratado de crear la consulta y su carga de los primeros 30 registros, pero no puedo entender cómo puedo solicitar nuevos registros.

Mi código de actividad es:

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); } } 

ACTUALIZAR:

He añadido 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); } }); 

He intentado a MergeCursor en adaptador pero conseguir 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) 

Editado (Código ADAPTADOR):

 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); } } } 

Cualquier persona puede ayudar por favor, Gracias de antemano.

  • ¿Qué para establecer CursorAdapter (Contexto contexto, Cursor c, int flags) a fin de hacer que funcione con CursorLoader?
  • Mantenimiento de un ListView donde cada elemento de lista tiene una relación de uno a muchos
  • 2 Solutions collect form web for “Android RecyclerView + CursorLoader + ContentProvider + "Cargar más"”

    Aquí está una muestra de trabajo con paginación basada en cursoradapter + recyclerview + provider.

    Te doy paso a paso con el código + bonificación con vista previa gif.

    Pero paginación IMHO en el adaptador de cursor tiene sentido porque db está manejando todas las cosas pesadas con la carga de más datos 🙂

    Paso 1. crear base de datos:

     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); } } 

    Paso 2. crear tabla

     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}; } 

    Paso 3. crear proveedor

     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; } } 

    Paso 4. crear un adaptador de cursor abstracto, tomé muestras de 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); } } } 

    Paso 5. crear su propio adaptador con extender (heredar) la clase anterior

     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; } } 

    Paso 6. crear viewHolder personalizado

     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"))); } } 

    Paso 7. escribir código en la muestra 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"; } 

    Paso 8. declarar proveedor en 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> 

    Paso 9. crear xml para la clase MainActivity

     <?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> 

    Pruébalo:

    El gatillo para cargar más es cada elemento del elemento 30, así que si el índice empieza desde 0, 29 será el disparador.

    Introduzca aquí la descripción de la imagen

    Recientemente, creé un TodoApp que contiene la mayoría de las funciones requeridas. Enlace aquí

    La aplicación contiene las siguientes características relevantes:

    1) RecyclerView encargo con la ayuda del Cursor .

    2) Proveedor de contenido para realizar operaciones CRUD básicas en la base de datos SQLite.

    3) AsyncQueryHandler para interactuar fácilmente con el proveedor de contenido.

    4) CursorLoader que actualiza el RecyclerView tan pronto como cambia la base de datos subyacente.

    Lo único que queda es implementar carga más funcionalidad. Codepath tiene un muy buen artículo sobre RecyclerView con Load More . (Dejarme saber si cualquier clase de ayuda se requiere en esto :))

    FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.