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);
}
}
Related
In my app I use a content provider. As you know the content provider is the middle man between the client and SQLite. In my case I retrieve the data from a server using volley,store them in SQLite, and finally read them using the ContentResolver object and the LoaderManager interface(which has onCreateLoader,onLoadFinished,onLoaderReset). I also use a service, as I want to run my webservice, when the app is closed.
MyService
public class MyService extends IntentService {
private final String LOG_TAG = MyService.class.getSimpleName();
public MyService() {
super("My Service");
}
#Override
protected void onHandleIntent(Intent intent) {
updateCityList();
}
#Override
public IBinder onBind(Intent intent) {
return null;
}
public void updateCityList() {
cityList.clear();
// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
// Request a string response from the provided URL.
JsonArrayRequest jsObjRequest = new JsonArrayRequest(Request.Method.GET,
API.API_URL, new Response.Listener<JSONArray>() {
#Override
public void onResponse(JSONArray response) {
Log.d(TAG, response.toString());
//hidePD();
// Parse json data.
// Declare the json objects that we need and then for loop through the children array.
// Do the json parse in a try catch block to catch the exceptions
try {
for (int i = 0; i < response.length(); i++) {
JSONObject post = response.getJSONObject(i);
MyCity item = new MyCity();
item.setName(post.getString("title"));
item.setImage(API.IMAGE_URL + post.getString("image"));
ContentValues imageValues = new ContentValues();
imageValues.put(MyCityContract.MyCityEntry._ID, post.getString("id"));
imageValues.put(MyCityContract.MyCityEntry.COLUMN_NAME, post.getString("title"));
imageValues.put(MyCityContract.MyCityEntry.COLUMN_ICON, post.getString("image"));
getContentResolver().insert(MyCityContract.MyCityEntry.CONTENT_URI, imageValues);
cityList.add(item);
cityList.add(item);
}
} catch (JSONException e) {
e.printStackTrace();
}
// Update list by notifying the adapter of changes
myCityAdpapter.notifyDataSetChanged();
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
VolleyLog.d(TAG, "Error: " + error.getMessage());
//hidePD();
}
});
queue.add(jsObjRequest);
}
static public class AlarmReceiver extends BroadcastReceiver{
#Override
public void onReceive(Context context, Intent intent) {
Intent sendIntent = new Intent(context, MyService.class);
context.startService(sendIntent);
}
}
}
MainActivityFragment
public class MainActivityFragment extends Fragment implements
LoaderManager.LoaderCallbacks<Cursor>{
static public ArrayList<MyCity> cityList;
public String [] MY_CITY_PROJECTIONS = {MyCityContract.MyCityEntry._ID,
MyCityContract.MyCityEntry.COLUMN_NAME,
MyCityContract.MyCityEntry.COLUMN_ICON};
private static final String LOG_TAG =
MainActivityFragment.class.getSimpleName();
public static MyCityAdpapter myCityAdpapter;
private static final int CURSOR_LOADER_ID = 0;
private GridView mGridView;
public MainActivityFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Add this line in order for this fragment to handle menu events.
setHasOptionsMenu(true);
}
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.refresh, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_refresh) {
return true;
}
return super.onOptionsItemSelected(item);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
// inflate fragment_main layout
final View rootView = inflater.inflate(R.layout.fragment_main_activity, container, false);
cityList = new ArrayList<>();
// initialize our FlavorAdapter
myCityAdpapter = new MyCityAdpapter(getActivity(), null, 0, CURSOR_LOADER_ID);
// initialize mGridView to the GridView in fragment_main.xml
mGridView = (GridView) rootView.findViewById(R.id.flavors_grid);
// set mGridView adapter to our CursorAdapter
mGridView.setAdapter(myCityAdpapter);
Cursor c =
getActivity().getContentResolver().query(MyCityContract.MyCityEntry.CONTENT_URI,
new String[]{MyCityContract.MyCityEntry._ID},
null,
null,
null);
if (c.getCount() == 0){
updateCityData();
}
// initialize loader
getLoaderManager().initLoader(CURSOR_LOADER_ID, null, this);
super.onCreate(savedInstanceState);
return rootView;
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args){
return new CursorLoader(getActivity(),
MyCityContract.MyCityEntry.CONTENT_URI,
MY_CITY_PROJECTIONS,
null,
null,
null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
myCityAdpapter.swapCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader){
myCityAdpapter.swapCursor(null);
}
public void updateCityData() {
Intent alarmIntent = new Intent(getActivity(), MyService.AlarmReceiver.class);
//Wrap in a pending intent which only fires once.
PendingIntent pi = PendingIntent.getBroadcast(getActivity(), 0,alarmIntent,PendingIntent.FLAG_ONE_SHOT);//getBroadcast(context, 0, i, 0);
AlarmManager am=(AlarmManager)getActivity().getSystemService(Context.ALARM_SERVICE);
//Set the AlarmManager to wake up the system.
am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, pi);
}
}
I just setup an alarm manager to make my service run after 5 seconds. This is just for testing really. Anyway,here is my problem. When I launch the app for the first time,nothing in shown in my screen. When I exit though,and launch it again,I can see all the images in my gridview. Why is this happening? To make more clear
When I launch the app for the first time:
10-16 12:07:00.799 16685-16685/theo.testing.androidcustomloaders D/ContentValues: [{"id":"15","title":"The Gate of Larissa","image":"larissa17.png"},{"id":"14","title":"Larissa Fair","image":"larissa14.png"},{"id":"13","title":"Larissa Fair","image":"larissa13.png"},{"id":"12","title":"AEL FC Arena","image":"larissa12.png"},{"id":"11","title":"AEL FC Arena","image":"larissa11.png"},{"id":"10","title":"Alcazar Park","image":"larissa10.png"},{"id":"9","title":"Alcazar Park","image":"larissa9.png"},{"id":"8","title":"Church","image":"larissa8.png"},{"id":"7","title":"Church","image":"larissa7.png"},{"id":"6","title":"Old trains","image":"larissa6.png"},{"id":"5","title":"Old trains","image":"larissa5.png"},{"id":"4","title":"Munipality Park","image":"larissa4.png"},{"id":"3","title":"Munipality Park","image":"larissa3.png"},{"id":"2","title":"Ancient Theatre - Larissa","image":"larissa2.png"},{"id":"1","title":"Ancient Theatre - Larissa","image":"larissa1.png"}]
In order to display the data I need to exit the app and launch it again. Why is this happening? Is there something wrong with my code?
LoadManager doesn't handle Your changes in database because it doesn't have any connection to it. You must register observer to handle that stuff.
In Your myCityProvider, in query(...) method is missing method setNotificationUri. It should be set at the end.
Here is modified Your query method:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor retCursor;
switch (sUriMatcher.match(uri)) {
// All Flavors selected
case MY_CITY: {
retCursor = myCityDbHelper.getReadableDatabase().query(
MyCityContract.MyCityEntry.TABLE_MY_CITY,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
break;
}
// Individual flavor based on Id selected
case MY_CITY_WITH_ID: {
retCursor = myCityDbHelper.getReadableDatabase().query(
MyCityContract.MyCityEntry.TABLE_MY_CITY,
projection,
MyCityContract.MyCityEntry._ID + " = ?",
new String[]{String.valueOf(ContentUris.parseId(uri))},
null,
null,
sortOrder);
break;
}
default: {
// By default, we assume a bad URI
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
if (retCursor != null) {
retCursor.setNotificationUri(getContext().getContentResolver(), uri);
}
return retCursor;
}
I've checked Your git repo and I think You should fix Your MainActivityFragment. You do everyting in onCreateView but You should do here all stuff related to view or just return inflated view. And after that, You can do the rest in onViewCreated.
You should do In this way:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_main_activity, container, false);
}
#Override
public void onViewCreated(View rootView, #Nullable Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
myCityAdpapter = new MyCityAdpapter(getActivity(), null, 0);
mGridView = (GridView) rootView.findViewById(R.id.flavors_grid);
mGridView.setAdapter(myCityAdpapter);
getLoaderManager().initLoader(CURSOR_LOADER_ID, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case CURSOR_LOADER_ID:
return new CursorLoader(getActivity(),
MyCityContract.MyCityEntry.CONTENT_URI,
null,
null,
null,
null);
default:
throw new IllegalArgumentException("id not handled!");
}
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
switch (loader.getId()) {
case CURSOR_LOADER_ID:
if (data == null || data.getCount() == 0) {
updateCityData();
} else {
myCityAdpapter.swapCursor(data);
}
break;
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
myCityAdpapter.swapCursor(null);
}
Thanks to that:
if app opens, loader will load all what he have (can be 0 items) but if there aren't any items it will call service to download more and store in db
if you add any data by service, onLoadFinished will be called again and refresh adapter
I have written a program to update my listview using SimpleCursorAdapter.
I have an EditText to enter mobile number and when I click on add , the mobile number is getting stored inside my sqlite database and at the same time displaying it inside the listview. But the problem is the listview is not getting refreshed , I have tried using notifyDataSetChanged() but not working.
Home_Page.java : This is the activity where I am updating my listview
try{
dbListHelper = new DriverSqliteHelper(getBaseContext());
dbListHelper.open(getBaseContext());
}catch (Exception e){
e.printStackTrace();
}
columns = new String[]{DriverSqliteHelper.DbListHelper.DRIVER_USER_ID};
to = new int[]{R.id.DriverId};
getLoaderManager().initLoader(0, null, this);
columns = new String[]{DriverSqliteHelper.DbListHelper.DRIVER_USER_ID};
to = new int[]{R.id.DriverId};
driverStatusAdapter = new DriverStatusAdapter(getBaseContext(),
R.layout.view_userid_item,null,columns,to,0);
listDriverId.setAdapter(driverStatusAdapter);
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
return new CustomCursorLoader(getBaseContext());
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if(driverStatusAdapter!=null && cursor!=null) {
driverStatusAdapter.swapCursor(cursor);
driverStatusAdapter.notifyDataSetChanged();
}else {
Log.v("Adapter null","OnLoadFinished: mAdapter is null");
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
if(driverStatusAdapter!=null) {
driverStatusAdapter.swapCursor(null);
} else {
Log.v("Adapter null", "OnLoadReset: mAdapter is null");
}
}
#Override
protected void onResume() {
super.onResume();
getLoaderManager().restartLoader(0x01, null, this);
}
I have used CustomCursorLoader class for querying the results.
public class CustomCursorLoader extends CursorLoader{
Context context;
DriverSqliteHelper driverSqliteHelper;
Cursor cursor;
public CustomCursorLoader(Context context) {
super(context);
try {
driverSqliteHelper = new DriverSqliteHelper(context);
driverSqliteHelper.open(context);
}catch (Exception e){
e.printStackTrace();
}
}
public Cursor loadInBackground(){
cursor = driverSqliteHelper.getDriverStatus();
return cursor;
}
}
When I restart my app the listview is updated but not automatically when I click on add button from the same activity.
I have been stuck on this for a long time and I don't know what to do now to refresh the listview whenever I click on add.
Any help is appreciated.
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'm trying to use an AsyncTaskLoader to load data in the background to populate a detail view in response to a list item being chosen. I've gotten it mostly working but I'm still having one issue. If I choose a second item in the list and then rotate the device before the load for the first selected item has completed, then the onLoadFinished() call is reporting to the activity being stopped rather than the new activity. This works fine when choosing just a single item and then rotating.
Here is the code I'm using. Activity:
public final class DemoActivity extends Activity
implements NumberListFragment.RowTappedListener,
LoaderManager.LoaderCallbacks<String> {
private static final AtomicInteger activityCounter = new AtomicInteger(0);
private int myActivityId;
private ResultFragment resultFragment;
private Integer selectedNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myActivityId = activityCounter.incrementAndGet();
Log.d("DemoActivity", "onCreate for " + myActivityId);
setContentView(R.layout.demo);
resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
super.onDestroy();
Log.d("DemoActivity", "onDestroy for " + myActivityId);
}
#Override
public void onRowTapped(Integer number) {
selectedNumber = number;
resultFragment.setResultText("Fetching details for item " + number + "...");
getLoaderManager().restartLoader(0, null, this);
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new ResultLoader(this, selectedNumber);
}
#Override
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
resultFragment.setResultText(data);
}
#Override
public void onLoaderReset(Loader<String> loader) {
}
static final class ResultLoader extends AsyncTaskLoader<String> {
private static final Random random = new Random();
private final Integer number;
private String result;
ResultLoader(Context context, Integer number) {
super(context);
this.number = number;
}
#Override
public String loadInBackground() {
// Simulate expensive Web call
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
}
#Override
public void deliverResult(String data) {
if (isReset()) {
// An async query came in while the loader is stopped
return;
}
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
// Only do a load if we have a source to load from
if (number != null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
result = null;
}
}
}
List fragment:
public final class NumberListFragment extends ListFragment {
interface RowTappedListener {
void onRowTapped(Integer number);
}
private RowTappedListener rowTappedListener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
rowTappedListener = (RowTappedListener) activity;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
R.layout.simple_list_item_1,
Arrays.asList(1, 2, 3, 4, 5, 6));
setListAdapter(adapter);
}
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
rowTappedListener.onRowTapped(adapter.getItem(position));
}
}
Result fragment:
public final class ResultFragment extends Fragment {
private TextView resultLabel;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.result_fragment, container, false);
resultLabel = (TextView) root.findViewById(R.id.result_label);
if (savedInstanceState != null) {
resultLabel.setText(savedInstanceState.getString("labelText", ""));
}
return root;
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("labelText", resultLabel.getText().toString());
}
void setResultText(String resultText) {
resultLabel.setText(resultText);
}
}
I've been able to get this working using plain AsyncTasks but I'm trying to learn more about Loaders since they handle the configuration changes automatically.
EDIT: I think I may have tracked down the issue by looking at the source for LoaderManager. When initLoader is called after the configuration change, the LoaderInfo object has its mCallbacks field updated with the new activity as the implementation of LoaderCallbacks, as I would expect.
public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);
if (info == null) {
// Loader doesn't already exist; create.
info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback);
if (DEBUG) Log.v(TAG, " Created new loader " + info);
} else {
if (DEBUG) Log.v(TAG, " Re-using existing loader " + info);
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}
if (info.mHaveData && mStarted) {
// If the loader has already generated its data, report it now.
info.callOnLoadFinished(info.mLoader, info.mData);
}
return (Loader<D>)info.mLoader;
}
However, when there is a pending loader, the main LoaderInfo object also has an mPendingLoader field with a reference to a LoaderCallbacks as well, and this object is never updated with the new activity in the mCallbacks field. I would expect to see the code look like this instead:
// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
It appears to be because of this that the pending loader calls onLoadFinished on the old activity instance. If I breakpoint in this method and make the call that I feel is missing using the debugger, everything works as I expect.
The new question is: Have I found a bug, or is this the expected behavior?
In most cases you should just ignore such reports if Activity is already destroyed.
public void onLoadFinished(Loader<String> loader, String data) {
Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
if (isDestroyed()) {
Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
return;
}
resultFragment.setResultText(data);
}
Also you should insert checking isDestroyed() in any inner classes. Runnable - is the most used case.
For example:
// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
public void run() {
// some heavy operations
...
// notification to UI thread
handler.post(new Runnable() {
// this runnable can link to 'dead' activity or any outer instance
if (isDestroyed()) {
return;
}
// we are alive
onSomeHeavyOperationFinished();
});
}
});
But in such cases the best way is to avoid passing strong reference on Activity to another thread (AsynkTask, Loader, Executor, etc).
The most reliable solution is here:
// BackgroundExecutor.java
public class BackgroundExecutor {
private static final Executor instance = Executors.newSingleThreadExecutor();
public static void execute(Runnable command) {
instance.execute(command);
}
}
// MyActivity.java
public class MyActivity extends Activity {
// Some callback method from any button you want
public void onSomeButtonClicked() {
// Show toast or progress bar if needed
// Start your heavy operation
BackgroundExecutor.execute(new SomeHeavyOperation(this));
}
public void onSomeHeavyOperationFinished() {
if (isDestroyed()) {
return;
}
// Hide progress bar, update UI
}
}
// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
private final WeakReference<MyActivity> ref;
public SomeHeavyOperation(MyActivity owner) {
// Unlike inner class we do not store strong reference to Activity here
this.ref = new WeakReference<MyActivity>(owner);
}
public void run() {
// Perform your heavy operation
// ...
// Done!
// It's time to notify Activity
final MyActivity owner = ref.get();
// Already died reference
if (owner == null) return;
// Perform notification in UI thread
owner.runOnUiThread(new Runnable() {
public void run() {
owner.onSomeHeavyOperationFinished();
}
});
}
}
Maybe not best solution but ...
This code restart loader every time, which is bad but only work around that works - if you want to used loader.
Loader l = getLoaderManager().getLoader(MY_LOADER);
if (l != null) {
getLoaderManager().restartLoader(MY_LOADER, null, this);
} else {
getLoaderManager().initLoader(MY_LOADER, null, this);
}
BTW. I am using Cursorloader ...
A possible solution is to start the AsyncTask in a custom singleton object and access the onFinished() result from the singleton within your Activity. Every time you rotate your screen, go onPause() or onResume(), the latest result will be used/accessed. If you still don't have a result in your singleton object, you know it is still busy or that you can relaunch the task.
Another approach is to work with a service bus like Otto, or to work with a Service.
Ok I'm trying to understand this excuse me if I misunderstood anything, but you are losing references to something when the device rotates.
Taking a stab...
would adding
android:configChanges="orientation|keyboardHidden|screenSize"
in your manifest for that activity fix your error? or prevent onLoadFinished() from saying the activity stopped?
I've got some code which queries a rest api on a service which then updates a database, I then have a cursor which looks at the database. I got some of the underlaying framework from the google iosched app.
Calls to mRunnersAdapter.notifyDataSetChanged() in the onReceiveResult method don't seem to do anything, it's only by manually initiating a query with mRunnerHandler.startQuery in the Runnable mRefreshRunnersRunnable does the data update. I think there's something wrong here, I'm sure I shouldn't need to restart the query again but I can't seem to get anything else to work.
Can anyone see where I'm going wrong?
public class exampleActivity extends Activity implements DetachableResultReceiver.Receiver {
public void onCreate(Bundle savedInstanceState) {
mState = (AppState) activity.getApplication();
mState.mReceiver.setReceiver(this);
mRunnerHandler = new NotifyingAsyncQueryHandler(getContentResolver(), runnersListener);
mRunnersAdapter = new RunnerAdapter(this);
setListAdapter(mRunnersAdapter);
refreshRunnerPriceInfo();
}
public void resetTimer() {
nextRefreshTimePeriod = (SystemClock.uptimeMillis() / refreshPeriod + 1) * refreshPeriod;
}
public void refreshRunnerPriceInfo() {
resetTimer();
getRunnerPriceInfo();
}
private void getRunnerPriceInfo() {
Intent serviceIntent = new Intent(Intent.ACTION_SYNC, null, getBaseContext(), QueryService.class);
serviceIntent.putExtra(QueryService.EXTRA_STATUS_RECEIVER, mState.mReceiver);
serviceIntent.putExtra(QueryService.EXTRA_STATUS_URL_EXTENSION, Price.buildUrlExtension(marketId));
serviceIntent.putExtra(QueryService.EXTRA_STATUS_TYPE, Price.CONTENT_TYPE);
startService(serviceIntent);
}
public void onWindowFocusChanged(boolean hasFocus) {
if (!hasFocus) {
nextRefreshTimePeriod = -1;
} else {
refreshRunnerPriceInfo();
}
super.onWindowFocusChanged(hasFocus);
}
AsyncQueryListener runnersListener = new AsyncQueryListener() {
public void onQueryComplete(int token, Object cookie, Cursor cursor) {
startManagingCursor(cursor);
mRunnersAdapter.changeCursor(cursor);
}
};
private Runnable mRefreshRunnersRunnable = new Runnable() {
public void run() {
if (queriesStarted) {
getRunnerPriceInfo();
resetTimer();
mRunnerHandler.startQuery(Runner.buildUri(marketId), RunnerPriceQuery.PROJECTION, Price.DEFAULT_SORT);
mMessageHandler.postAtTime(mRefreshRunnersRunnable, nextRefreshTimePeriod);
}
}
};
private class RunnerAdapter extends CursorAdapter implements Filterable {
public RunnerAdapter(Context context) {
super(context, null);
}
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return getLayoutInflater().inflate(R.layout.market_view_list_item, parent, false);
}
public void bindView(View view, Context context, Cursor cursor) {
// populate view
}
}
protected void onResume() {
super.onResume();
mMessageHandler.post(mRefreshRunnersRunnable);
}
protected void onPause() {
mMessageHandler.removeCallbacks(mRefreshRunnersRunnable);
super.onPause();
}
interface RunnerPriceQuery {
String[] PROJECTION = { BaseColumns._ID, etc };
}
public void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case QueryService.STATUS_RUNNING: {
break;
}
case QueryService.STATUS_FINISHED: {
String intentReturnType;
try {
intentReturnType = resultData.getString(QueryService.EXTRA_STATUS_TYPE);
} catch (NullPointerException e) {
BLog.e(getClass(), "No results found, probably network issues", e);
break;
}
if (Price.CONTENT_TYPE.equals(intentReturnType)) {
if (!queriesStarted) {
mMessageHandler.post(mRefreshRunnersRunnable);
mRunnerHandler.startQuery(Runner.buildUri(marketId), RunnerPriceQuery.PROJECTION, Price.DEFAULT_SORT);
queriesStarted = true;
}
if (mRunnersAdapter != null)
mRunnersAdapter.notifyDataSetChanged();
}
break;
}
case QueryService.STATUS_ERROR: {
final String errorText = getString(R.string.toast_sync_error, resultData.getString(Intent.EXTRA_TEXT));
Log.i(this.getClass(), "STATUS_ERROR\n" + errorText);
Toast.makeText(MarketActivity.this, errorText, Toast.LENGTH_LONG).show();
break;
}
}
}
}