I need to show some data from server. There are several fragments where items can be displayed, so it is not easy to mantain updates. I decided to cache data in DB and use ContentProvider to access it. But I have a strange issue.
Initially, there is no data in my database. I create CursorLoader and set CursorAdapter as adapter to ListFragment. Then I receive something from server and insert several records. I supposed Loader to be automatically notified about them and reload data. But nothing happens, and ListFragment stays empty.
The other strange thing related. I've wrote some code to add ActionBar menu for this fragment. In this situation it onCreateOptionsMenu is not called (despite of setHasOptionsMenu(true) in onCreate) and menu not added. When DB already has some data, it works normally.
It seems that something broken inside the Fragment, but there is nothing about this in logs. What do you think, what I am doing wrong?
Here is sample fragment, that works like this:
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.Menu;
import android.view.MenuInflater;
public class FeedFragment3 extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 1;
private SimpleCursorAdapter mAdapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
#Override
public void onResume() {
super.onResume();
getActivity().getActionBar().setTitle(R.string.feed_ab_title);
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.feed, menu);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_1, null,
new String[] {"title"},
new int[] {android.R.id.text1}
);
setListAdapter(mAdapter);
getActivity().getSupportLoaderManager().initLoader(LOADER_ID, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(getActivity(), Consts.getFeedContentUri(), null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.changeCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.changeCursor(null);
}
}
Here is a part of ContentProvider. I suppose that calling notifyChange in insert and setNotificationUri in query make ContentProvider notified about changes. May be it is not enough?
public class FeedContentProvider extends ContentProvider {
...
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
DPLog.it(TAG, "Query: [%s]", uri);
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables("feed");
qb.setProjectionMap(mProjectionMap);
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
DPLog.it(TAG, "insert: [%s]", uri);
String tableName = mTableNamesForCode.get(mUriMatcher.match(uri));
mDb.getWritableDatabase().insert(tableName, null, values);
getContext().getContentResolver().notifyChange(uri, null);
return uri;
}
...
}
Opening fragment in Activity:
private void openFeedFragment() {
mFeedFragment = new FeedFragment3();
showRootFragment(mFeedFragment);
updateFeed();
}
private void updateFeed() {
mApi.requestFeed(
new Response.Listener<FeedResponse>() {
#Override
public void onResponse(FeedResponse response) {
Uri uri = PostsContract.getFeedContentUri();
Set<Long> existingIds = getPostIds(uri);
ArrayList<ContentProviderOperation> operations = createMergeOperations();
try {
getContentResolver().applyBatch(PostsContract.AUTHORITY, operations);
} catch (RemoteException e) {
...
} catch (OperationApplicationException e) {
...
}
}
},
null
);
}
Logs when creating Fragment
03-21 19:12:35.041 22005-22344/com.app D/app.ContentProvider﹕ Cursor size: [0] 03-21 19:12:35.061 22005-22005/com.app V/app.checkpoint-checkpoint﹕ com.app.ui.fragment.FeedFragment3.onLoadFinished (FeedFragment3.java:60)
03-21 19:12:36.012 22005-22066/com.app D/app.API﹕ Request 'feed' finished
03-21 19:12:36.092 22005-22066/com.app D/dalvikvm﹕ GC_FOR_ALLOC freed 1417K, 11% free 12805K/14256K, paused 25ms, total 25ms
03-21 19:12:36.122 22005-22005/com.app I/app.ContentProvider﹕ Query: [content://com.app.content.posts/feed] (PostsContentProvider.java:59)
03-21 19:12:36.122 22005-22005/com.app D/app.ContentProvider﹕ Cursor size: [0] (PostsContentProvider.java:78)
03-21 19:12:36.142 22005-22005/com.app I/app.ContentProvider﹕ insert: [content://com.app.content.posts/feed] (PostsContentProvider.java:97)
03-21 19:12:36.262 22005-22005/com.app D/app.ContentProvider﹕ Inserted id: [1] (PostsContentProvider.java:102)
...
03-21 19:12:37.453 22005-22005/com.app D/app.ContentProvider﹕ Inserted id: [21] (PostsContentProvider.java:102)
03-21 19:12:37.453 22005-22348/com.app I/app.ContentProvider﹕ Query: [content://com.app.content.posts/feed] (PostsContentProvider.java:59)
03-21 19:12:37.453 22005-22348/com.app D/app.ContentProvider﹕ Cursor size: [21] (PostsContentProvider.java:78)
03-21 19:12:37.463 22005-22073/com.app I/app.ContentProvider﹕ Query: [content://com.app.content.posts/feed] (PostsContentProvider.java:59)
03-21 19:12:37.463 22005-22073/com.app D/app.ContentProvider﹕ Cursor size: [21] (PostsContentProvider.java:78)
03-21 19:12:37.463 22005-22005/com.app V/app.checkpoint-checkpoint﹕ com.app.ui.fragment.FeedFragment3.onLoadFinished (FeedFragment3.java:60)
in your code :-
use swapCursor(data) instead of changeCursor(data)
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(getActivity(), Consts.getFeedContentUri(), null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.swapCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
try it
for the first problem the error is the constructor of SimpleCursorAdapter that is deprecated and you must say to listen the changes like this:
mAdapter = new SimpleCursorAdapter(getActivity(),
android.R.layout.simple_list_item_1,
null,
new String[] {"title"},
new int[] {android.R.id.text1},
SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER)
);
for the second you try this that works for me:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
[...]
}
in the end NOT use getActivity().getActionBar() but getSupportActionBar if you are using
import android.support.v4.app.ListFragment;
EDIT:
improve and fix your loader:
#Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
switch (cursorLoader.getId()) {
case LOADER_ID:
mAdapter.swapCursor(cursor);
break;
}
}
Not sure if this should make a difference, but use the fragment's LoaderManager instead of the activity's.
Replace
getActivity().getSupportLoaderManager().initLoader(LOADER_ID, null, this);
with
getLoaderManager().initLoader(LOADER_ID, null, this);
Related
I tried to follow this tutorial to implement CursorLoader for listview in fragment. Everything is fine when scrolling in listview (even fast scroll, it very smooth), except the first time I switch to this fragment, cursor loader load slow a bit. Does any one know how to speed it up? here is my try:
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
String[] projection = { DatabaseHandler.UserTable.id, DatabaseHandler.UserTable.name };
cursorLoader = new CursorLoader(this, DatabaseAccessUtility.CONTENT_URI, projection, null, null, null);
return cursorLoader;
}
public void onLoadFinished(Loader<Cursor> loader,Cursor cursor) {
if(mAdapter!=null && cursor!=null)
mAdapter.swapCursor(cursor);
public void onLoaderReset(Loader<Cursor> arg0) {
if(mAdapter!=null)
mAdapter.swapCursor(null);
}
I have finally after several tutorials make my CursorLoader display in my ListView but I have some questions that I hope you guys can help me to solve.
1) my big problem was this:
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if(uriMatcher.match(uri)==CUSTOMERS){
DB.open();
return DB.leer();
}
else
{
return null;
}
}
as you can see this is from my Content Provider class, my problem was that I dont figure I need to OPEN my DB.
when I put the code for open the DB works for me but then I dont know if I need to close here or it close after doing this:
#Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1){
Uri uri = Contenido.CONTENT_URI;
return new CursorLoader(this, uri, null, null, null, null);
}
/** A callback method, invoked after the requested content provider returned all the data */
#Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
mAdapter.swapCursor(arg1);
}
#Override
public void onLoaderReset(Loader<Cursor> arg0) {
mAdapter.swapCursor(null);
}
should I put DB.close() on any place of this functions?
Or maybe below this:
getSupportLoaderManager().initLoader(0, null, this);
getSupport is on the OnCreate Bundle. I really don't understand if its necesary close the db that is open from the Cursor query.
I am facing an issue with Loader.
I have an Activity, which displays list of records retrieved from local DB. When the activity starts, records are automatically loaded via LoaderManager.initLoader() method.
There is also possibility to manually refresh the list via refresh button in ActionBarSherlock. However, after finishing another activity which adds a record to DB, onLoadFinished is not called.
I am using SimpleCursorLoader and here is code snippet from the activity:
#Override
public void onStart() {
...
getSupportLoaderManager().initLoader(0, null, this);
}
#Override
public void onPause() {
...
getSupportLoaderManager().destroyLoader(0);
}
public void refreshRecords() {
getSupportLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, final Bundle args) {
Loader<Cursor> l = new SimpleCursorLoader(this) {
#Override
public Cursor loadInBackground() {
return recordDAO.getCursor();
}
};
l.forceLoad();
return l;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
// updateUI
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
The issue is that after finishing the other activity, onLoaderCreate is called, but onLoaderFinished is not called.
after some debugging, I've found that SimpleCursorAdapter.deliverResults() is also called, bud ends up on .. if (isReset()) { ..
Am I missing something? How to force the reload of data?
Thank you in advance
I have finally found the solution to this problem thanks to the discussion on
https://groups.google.com/forum/#!topic/android-developers/DbKL6PVyhLI
public static <T> void initLoader(final int loaderId, final Bundle args, final LoaderCallbacks<T> callbacks,
final LoaderManager loaderManager) {
final Loader<T> loader = loaderManager.getLoader(loaderId);
if (loader != null && loader.isReset()) {
loaderManager.restartLoader(loaderId, args, callbacks);
} else {
loaderManager.initLoader(loaderId, args, callbacks);
}
}
In addition as of support library 28 make sure that you don't call initLoader from within Fragment.onCreate(). As the updated documentation states
You typically initialize a Loader within the activity's onCreate() method, or within the fragment's onActivityCreated() method.
see https://developer.android.com/guide/components/loaders
RaB solution dont work for me
My worked Solution, was always destroy Loader before restart
Loader<Cursor> loader = mLoaderManager.getLoader(mKeyLoader);
if (loader != null)
{
mLoaderManager.destroyLoader(mKeyLoader);
}
mLoaderManager.restartLoader(mKeyLoader, args, this);
In addition to RaB's answer, if you are using a custom Loader, make sure that if you call super if you overwrite deliverResult():
#Override
public void deliverResult(D data) {
super.deliverResult(data); // <--onLoadFinished() will not be called if you don't call this
...
}
fwiw, I had a similar problem from attempting to immediately restart the loader a second time, before the first onLoadFinished was called, resulting in neither being called.
this worked for me:
if( loader == null )
loader = loaderMngr.initLoader(
0, null, myLoaderCallbacks
);
else if( loader.isAbandoned() )
return;
else
loaderMngr.restartLoader(
0, null, myLoaderCallbacks
);
Check the support library.Use this import android.support.v4.app. Don't use android.app.loadermanager.
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
Initialize part
LoaderManager mLoaderManager=getSupportLoaderManager();
LoaderManager.LoaderCallbacks<Cursor> mCursorLoaderCallbacks=new LoaderManager.LoaderCallbacks<Cursor>() {
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle cursor) {
return new CursorLoader(getActivity(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, COLUMNS_OF_INTEREST, null, null,
MediaStore.Video.Media.DATE_ADDED + " DESC");
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
};
mLoaderManager.initLoader(URL_LOADER_EXTERNAL, null, mCursorLoaderCallbacks);
I am receiving a "swapCursor(Cursor) is undefined" error when creating a CursorLoader. I have imported the android.support.v4 (app.LoaderManager, app.LoaderManager.Loader, Content.CursorLoader, content.Loader). Not sure what I can do to correct this issue. please advise.
loader:
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.app.ListFragment;
import android.support.v4.widget.CursorAdapter;
public class LoginList extends FragmentActivity implements OnClickListener,
AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {
private ListView loginList;
private Button webLogin;
private SimpleCursorAdapter adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login_listview);
loginList = (ListView)findViewById(R.id.loginlist);
loginList.setOnItemClickListener(this);
webLogin = (Button)findViewById(R.id.button3);
webLogin.setOnClickListener(this);
//Specify fileds to display in the list
String[] from = new String[] { ListProvider.COLUMN_NAME_SITE };
//Bind fields to listview
int[] to = new int[] {R.id.loginlist};
// Create CursorAdapter and set it to display
adapter = new SimpleCursorAdapter(this, R.layout.login_listview, null, from, to);
loginList.setAdapter(adapter);
getSupportLoaderManager().initLoader(0, null, this);
}
/*#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setEmptyText(getResources().getString(string.app_name));
rwgisterForContextMenu(getListView());
setHasOptionsMenu(true);
} */
#Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
// TODO Auto-generated method stub
Toast.makeText(getApplicationContext(), "Selected ID :" + arg2, Toast.LENGTH_SHORT).show();
Intent updateDeleteLoginInfo = new Intent (this, UpdateDeleteLoginList.class);
Cursor clickedObject = (Cursor)loginList.getItemAtPosition(arg2);
Bundle loginBundle = new Bundle();
loginBundle.putString("clickedWebSite",((LoginDetails) clickedObject).getsName());
loginBundle.putString("clickedWebAddress",((LoginDetails) clickedObject).getwUrl());
loginBundle.putString("clickedUserName",((LoginDetails) clickedObject).getuName());
loginBundle.putString("clickedPassWord",((LoginDetails) clickedObject).getpWord());
loginBundle.putString("clickedNotes",((LoginDetails) clickedObject).getlNotes());
updateDeleteLoginInfo.putExtras(loginBundle);
startActivityForResult(updateDeleteLoginInfo, 0);
}
#Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
Intent webLoginIntent = new Intent (this, LoginPlusActivity.class);
startActivity(webLoginIntent);
}
#Override
public Loader<Cursor> onCreateLoader(int ignored, final Bundle args) {
return new CursorLoader(this, ListProvider.CONTENT_URI, null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor); /* swapCursor error: The method swapCursor(Cursor) is undefined for the type SimpleCursorAdapter */
}
#Override
public void onLoaderReset (Loader<Cursor> loader) {
adapter.swapCursor(null); /* swapCursor error: The method swapCursor(Cursor) is undefined for the type SimpleCursorAdapter */
}
}
Example on how to use changeCursor() with LoaderManager.LoaderCallbacks instead of swapCursor().
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, DB_table.CONTENT_URI, DB_table.PROJECTION,null,null,null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.changeCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.changeCursor(null);
}
if you want to use the swapCursor() available in API 11 for older API's, remember to import the CursorAdapter from the support library.
import android.support.v4.widget.CursorAdapter;
In your onCreate() method you initialize your adapter passing the ListView as the second argument. This is incorrect, it should be the ID of a layout to instantiate for each item in the list:
adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, null, from, to);
Other than that your code looks fine to me. I've compared it against working code of mine that used a ListFragment so I don't instantiate a UI but override the onActivityCreated(Bundle) method.
I did take the trouble to provide a projection to cut down the amount of data extracted from database by my ContentProvider, but passing null shouldn't be a problem.
But do check that you provide the "_id" primary key column as a long in your ContentProvier. To use ListView (and some of the other widgets) you have to have a Long based primary key, which I found really inconvenient as I already had a perfectly acceptable UUID as my primary key.
I've got a CursorLoader in a ListFragment (that is inside a ViewPager) that queries a database. I have a content provider for that database, which I've verified works.
The issue is this: when the app runs for the very first time a separate service calls a bulk insert in a ContentProvider:
public int bulkInsert(Uri uri, ContentValues[] values) {
if(LOGV) Log.v(TAG, "insert(uri=" + uri + ", values" + values.toString() + ")");
final SQLiteDatabase db = openHelper.getWritableDatabase();
final int match = uriMatcher.match(uri);
switch(match) {
case SNAP: {
db.beginTransaction();
for(ContentValues cv : values) {
db.insertOrThrow(Tables.SNAP, null, cv);
}
db.setTransactionSuccessful();
db.endTransaction();
getContext().getApplicationContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
The CursorLoader in the list fragment returns 0 rows though on the very first run (when the database gets created). If I close and restart the app then the CursorLoader works great and returns exactly what I need. I've tried to implement waiting via a handler, but it doesn't seem to help. Here is the ListFragment that utilizes the CursorLoader:
public class DataBySnapFragment extends ListFragment implements LoaderCallbacks<Cursor> {
public static final String TAG = "DataBySnapFragment";
protected Cursor cursor = null;
private DataBySnapAdapter adapter;
private Handler handler = new Handler() {
#Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d(TAG, "RELOADING!!!!!");
onLoadDelay();
}
};
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, "onActivityCreated");
adapter = new DataBySnapAdapter(getActivity(),
R.layout.list_item_databysnap,
null,
new String[]{},
new int[]{},
0);
setListAdapter(adapter);
getLoaderManager().initLoader(0, null, this);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_databysnap, null);
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("error_workaround_1",
"workaroundforerror:Issue 19917,
http://code.google.com/p/android/issues/detail?id=19917");
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader cursorLoader = new CursorLoader(getActivity(),
Snap.CONTENT_URI,
null,
null,
null,
Snap.DATA_MONTH_TO_DATE + " DESC LIMIT 6");
return cursorLoader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Log.d(TAG, "data rows: " + data.getCount());
if(data.getCount() <= 0) {
delayThread();
} else {
adapter.swapCursor(data);
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
private void onLoadDelay() {
getLoaderManager().initLoader(0, null, this);
}
private void delayThread() {
new Thread() {
public void run() {
longTimeMethod();
handler.sendEmptyMessage(0);
}
}.start();
}
private void longTimeMethod() {
try {
Thread.sleep(12000);
} catch (InterruptedException e) {
Log.e("tag", e.getMessage());
}
}
}
Can anyone let me know why this might be happening, or at least steer me in the right direction? Thanks!
Unless you were stalling the main thread, making a new thread and telling it to sleep wouldn't really solve anything.
I can't really say what exactly might be wrong, but it sounds like you might need to refresh your data. To test you could try to make a button with an OnClickListener which refreshes the data content, and re-pull it from the database.
Whenever your bulk insert is done you should send a message back to the fragment/activity implementing LoaderManager.LoaderCallbacks interface.
When the activity/fragment receives the message you would need to call getLoaderManager().restartLoader(0, null, this) to requery the DB.
This is what the sample app does too http://developer.android.com/guide/topics/fundamentals/loaders.html
Try to modify onLoadFinished this way:
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Log.d(TAG, "data rows: " + data.getCount());
if(data.getCount() <= 0) {
delayThread();
} else {
// set the notification for the cursor
data.setNotificationUri(getActivity().getContentResolver(), Snap.CONTENT_URI);
adapter.swapCursor(data);
}
}