Android RecyclerView + CursorLoader + ContentProvider +“加载更多”

我创build了一个活动,我正在实施CursorLoader从数据库加载数据。

我已经为那个表的所有logging做了这件事,但我想加载30-30logging,如加载更多的function

我试图创build查询和其加载前30个logging,但我不能够了解如何请求新的logging。

我的活动代码是:

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

更新:

我已经添加了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); } }); 

我试图在适配器中的MergeCursor ,但得到错误:

 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) 

已编辑(ADAPTER代码):

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

任何人都可以帮忙,提前致谢。

Solutions Collecting From Web of "Android RecyclerView + CursorLoader + ContentProvider +“加载更多”"

更新01.10.2017谷歌宣布新的分页库,更多信息在这里https://developer.android.com/topic/libraries/architecture/paging.html


这里是基于cursoradapter + recyclerview +提供程序的分页工作示例。

我给你一步一步与GIF预览的代码+奖金。

但恕我直言分页游标适配器有胡说,因为数据库正在处理所有重负载更多的数据:)

步骤1.创build数据库:

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

第2步。创build表

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

第3步。创build提供者

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

第4步。创build一个抽象的游标适配器,我从StackOverflow自定义游标回收器视图适配器的示例

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

第5步。创build自己的适配器,扩展(inheritance)以前的类

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

第6步。创build自定义的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"))); } } 

第7步。在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"; } 

步骤8.在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> 

第9步。为MainActivity类创buildxml

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

testing它:

加载更多的触发器是每30个元素,所以如果索引从0开始,29将是触发器。

在这里输入图像说明

最近,我创build了一个TodoApp,其中包含大部分所需的function。 链接在这里

该应用程序包含以下相关function:

1)自定义RecyclerViewCursor支持。

2)Content Provider在SQLite数据库上进行基本的CRUD操作。

3) AsyncQueryHandler轻松地与内容提供者进行交互。

4)一旦底层数据库发生变化, CursorLoader就会立即更新RecyclerView

唯一剩下的就是实现加载更多的function。 带有更多加载function的 Codepath在RecyclerView上有非常好的文章。 (让我知道是否需要任何forms的帮助:))