J'ai créé une activité dans laquelle j'implémente CursorLoader pour charger les données de la base de données.
J'ai fait cette chose pour tous les enregistrements de cette table mais je veux charger 30-30 enregistrements comme Load More Functionality
J'ai essayé de créer une requête et son chargement des 30 premiers enregistrements, mais je ne peux pas comprendre comment puis-je demander de nouveaux enregistrements.
Mon code d'activité est comme:
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);
}
}
J'ai ajouté 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);
}
});
J'ai essayé de MergeCursor dans l'adaptateur mais j'obtiens une erreur:
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)
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);
}
}
}
Tout le monde peut vous aider, merci d'avance.
Mise à jour 01.10.2017 Google a annoncé une nouvelle bibliothèque de pagination, plus d'informations ici https://developer.Android.com/topic/libraries/architecture/paging.html
Voici un exemple de travail avec une pagination basée sur curseuradapter + recyclerview + provider.
Je vous donne étape par étape avec code + bonus avec aperçu gif.
Mais la pagination à mon humble avis sur l'adaptateur de curseur n'a pas de sens car db gère toutes les choses lourdes avec plus de données de chargement:)
Étape 1. créer une base de données:
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);
}
}
Étape 2. créer une 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};
}
Étape 3. créer un fournisseur
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;
}
}
Étape 4. créer un adaptateur de curseur abstrait, j'ai pris un échantillon 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);
}
}
}
Étape 5. créez votre propre adaptateur avec l'extension (héritant) de la classe précédente
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;
}
}
Étape 6. créer un viewHolder personnalisé
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")));
}
}
Étape 7. écrire du code dans l'exemple 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";
}
Étape 8. déclarer le fournisseur dans 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>
Étape 9. créer du xml pour la classe 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>
Testez-le:
Le déclencheur pour charger plus est chaque élément du 30ème élément, donc si l'index commence à 0, 29 sera le déclencheur.
Récemment, j'ai créé une TodoApp qui contient la plupart des fonctionnalités requises. lien ici
L'application contient les fonctionnalités pertinentes suivantes:
1) RecyclerView
personnalisé avec prise en charge de Cursor
.
2) Fournisseur de contenu pour effectuer des opérations CRUD de base sur la base de données SQLite.
3) AsyncQueryHandler pour interagir facilement avec le fournisseur de contenu.
4) CursorLoader
qui met à jour le RecyclerView
dès que la base de données sous-jacente change.
La seule chose qui reste est d'implémenter des fonctionnalités de chargement supplémentaires. Codepath a un très bon article sur RecyclerView avec Load More . (Faites-moi savoir si une aide quelconque est requise à ce sujet :))