データベースからデータをロードするためにCursorLoaderを実装しているという点で、1つのアクティビティを作成しました。
私はそのテーブルのすべてのレコードに対してそのことをしましたが、Load More Functionalityのような30-30レコードをロードしたいです
クエリとその読み込みの最初の30レコードを作成しようとしましたが、新しいレコードをリクエストする方法を理解できません。
私のアクティビティコードは次のようなものです:
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)
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);
}
}
}
誰でも助けてください、事前に感謝します。
2017年1月10日更新Googleはページング用の新しいライブラリを発表しました。詳細はこちら https://developer.Android.com/topic/libraries/architecture/paging.html
これは、cursoradapter + recyclerview + providerに基づくページネーションを使用した作業サンプルです。
コード+ボーナスとgifプレビューで段階を追って説明します。
しかし、dbはより多くのデータをロードしてすべての重いものを処理しているため、カーソルアダプタのIMHOページネーションはナンセンスです:)
ステップ1.データベースを作成します:
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.テーブルの作成
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.抽象カーソルアダプターを作成し、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);
}
}
}
ステップ5.前のクラスを拡張(継承)して独自のアダプターを作成する
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")));
}
}
ステップ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クラスのxmlを作成する
<?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がトリガーになります。
最近、ほとんどの必要な機能を含むTodoAppを作成しました。 リンクはこちら
アプリには、次の関連機能が含まれています。
1)RecyclerView
をサポートするカスタムCursor
。
2)SQLiteデータベースで基本的なCRUD操作を行うコンテンツプロバイダー。
3) AsyncQueryHandler は、コンテンツプロバイダーと簡単に対話します。
4)CursorLoader
は、基になるデータベースが変更されるとすぐにRecyclerView
を更新します。
残っているのは、より多くの機能を実装することだけです。 Codepathには RecyclerView with Load More に関する非常に良い記事があります。 (これについて何らかの支援が必要かどうか教えてください:))