Android RecyclerView + CursorLoader + ContentProvider + "Load More"
Android RecyclerView + CursorLoader + ContentProvider + "Load More"
我创建了一个 Activity,因为我正在实施 CursorLoader 以从数据库加载数据。
我已经为 Table 的所有记录做了那件事,但我想加载 30-30 条记录,例如 加载更多功能
我尝试创建查询并加载前 30 条记录,但我无法理解如何请求新记录。
我的 Activity 代码如下:
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@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);
}
}
}
任何人都可以提供帮助,在此先感谢。
最近,我创建了一个包含大部分必需功能的 TodoApp。
link here
该应用包含以下相关功能:
1) 支持 Cursor
的自定义 RecyclerView
。
2) Content Provider 在 SQLite 数据库上做基本的 CRUD 操作。
3) AsyncQueryHandler 轻松与内容提供商互动。
4) CursorLoader
一旦底层数据库发生变化就会更新 RecyclerView
。
唯一剩下的就是实现加载更多功能。 Codepath 在 RecyclerView with Load More 上有一篇非常好的文章。 (如果这方面需要任何帮助,请告诉我:))
更新 01.10.2017
Google 宣布了用于分页的新库,更多信息请点击此处 https://developer.android.com/topic/libraries/architecture/paging.html
这是基于 cursoradapter+recyclerview+provider 的分页工作示例。
我一步一步给你代码+奖金gif预览
但是恕我直言,游标适配器上的分页毫无意义,因为 db 正在处理所有繁重的东西并加载更多数据:)
第一步,创建数据库:
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. 创建 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};
}
步骤 3. 创建提供者
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 步。创建抽象游标适配器,我从 Whosebug 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);
}
}
}
步骤 5. 通过扩展(继承)之前的 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;
}
}
步骤 6. 创建自定义 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")));
}
}
第七步.在示例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 创建 xml 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>
测试一下:
加载更多的触发器是每第 30 个项目元素,因此如果索引从 0 开始,则 29 将是触发器。
我创建了一个 Activity,因为我正在实施 CursorLoader 以从数据库加载数据。
我已经为 Table 的所有记录做了那件事,但我想加载 30-30 条记录,例如 加载更多功能
我尝试创建查询并加载前 30 条记录,但我无法理解如何请求新记录。
我的 Activity 代码如下:
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@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);
}
}
}
任何人都可以提供帮助,在此先感谢。
最近,我创建了一个包含大部分必需功能的 TodoApp。 link here
该应用包含以下相关功能:
1) 支持 Cursor
的自定义 RecyclerView
。
2) Content Provider 在 SQLite 数据库上做基本的 CRUD 操作。
3) AsyncQueryHandler 轻松与内容提供商互动。
4) CursorLoader
一旦底层数据库发生变化就会更新 RecyclerView
。
唯一剩下的就是实现加载更多功能。 Codepath 在 RecyclerView with Load More 上有一篇非常好的文章。 (如果这方面需要任何帮助,请告诉我:))
更新 01.10.2017 Google 宣布了用于分页的新库,更多信息请点击此处 https://developer.android.com/topic/libraries/architecture/paging.html
这是基于 cursoradapter+recyclerview+provider 的分页工作示例。
我一步一步给你代码+奖金gif预览
但是恕我直言,游标适配器上的分页毫无意义,因为 db 正在处理所有繁重的东西并加载更多数据:)
第一步,创建数据库:
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. 创建 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};
}
步骤 3. 创建提供者
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 步。创建抽象游标适配器,我从 Whosebug 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);
}
}
}
步骤 5. 通过扩展(继承)之前的 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;
}
}
步骤 6. 创建自定义 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")));
}
}
第七步.在示例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 创建 xml 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>
测试一下:
加载更多的触发器是每第 30 个项目元素,因此如果索引从 0 开始,则 29 将是触发器。