Fragment's OnClickListener called after onDestroyView - android

I have an issue where ListFragment.onListItemClick is called after onDestroyView. I'm getting lots of error reports in the field (10-20 per day of ~1000 active users), but the only way I found to reproduce it is to hammer the back button while clicking all over the screen. Are hundreds of users really doing this?
This is the trace:
java.lang.IllegalStateException: Content view not yet created
at au.com.example.activity.ListFragment.ensureList(ListFragment.java:860)
at au.com.example.activity.ListFragment.getListView(ListFragment.java:695)
at au.com.example.activity.MyFragment.onListItemClick(MyFragment.java:1290)
at au.com.example.activity.ListFragment$2.onItemClick(ListFragment.java:90)
at android.widget.AdapterView.performItemClick(AdapterView.java:301)
at android.widget.AbsListView.performItemClick(AbsListView.java:1519)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3278)
at android.widget.AbsListView$1.run(AbsListView.java:4327)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5293)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
at dalvik.system.NativeStart.main(Native Method)
Caused from calling getListView().getItemAtPosition in MyFragment.onListItemClick (MyFragment:1290). How can getView return null during a click handler callback? I also determined the fragment was detached at this stage, isAdded() was false, and getActivity was null.
One workaround would be to replace getListView with the listView passed in from the callback
public void onListItemClick(ListView listView, View v, int position, long id), but other functions will still need to update other parts of the UI, so this would just move the problem somewhere else.
Instead, I nulled the callback in onDestroyView:
public void onDestroyView() {
mHandler.removeCallbacks(mRequestFocus);
if(mList!=null){
mList.setOnItemClickListener(null);
}
mList = null;
mListShown = false;
mEmptyView = mProgressContainer = mListContainer = null;
mStandardEmptyView = null;
super.onDestroyView();
}
But I still have this onClick problem in other (non-list) fragments too. How exactly does the framework suppress these callbacks normally when the fragment is removed (eg in onBackPressed -> popBackStackImmediate())?
In onDestroyView, I null out extra views that I created in onCreateView. Do I need to manually clear every listener I've set like this?
This is a similar issue to the unanswered q: Fragment's getView() returning null in a OnClickListener callback
I'm using setOnRetainInstance(true) in my fragments, btw.

You really haven't given very much information, but based off what you've given, it sounds like Fragment pending Transactions might be your issue.
In Android, whenever you are changing, or instantiating fragments, it's all done through Pending Transactions unless told to do otherwise. It's essentially a race condition.
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new ExampleFragment()
.commit();
The UI Thread has a queue of work that it needs to do at any given time. Even though you've committed the FragmentTransaction after running the above code, it's actually been queued on the UI Thread at the end of the queue, to happen after everything that is currently pending has been finished. What this means is that if click events happen while the transaction is pending (which can easily happen, i.e. you spamming clicks on the screen, or clicking with multiple fingers), those click events will be placed on the UI Threads queue after the FragmentTransaction.
The end result is that the Fragment Transaction is processed, your fragment View is destroyed, and then you call getView() and it returns null.
You could try a few things:
getSupportFragmentManager().executePendingTransactions() This will execute all pending transactions right then, and removes the pending aspect
Check to see if the Fragment isVisible() or isAdded() or some other fragment 'is' method that allows you to get runtime information about the current state the Fragment is in it's lifecycle, before you execute code that could potentially be run after the fragments view is destroyed (i.e. click listeners)
So lets say you have a click handler, where when the user clicks something you animate to another fragment. You could use something like the below piece of code that you run before the FragmentTransaction on your outermost view (in a Fragment, it'd be what returns from getView()), and that would either permanently disable clicks to a view if it was going to be destroyed, or temporarily disable clicks for a a period of time if you are going to re-use the view.
Hope this helps.
public class ClickUtil {
/**
* Disables any clicks inside the given given view.
*
* #param view The view to iterate over and disable all clicks.
*/
public static void disable(View view) {
disable(view, null);
}
/**
* Disables any clicks inside the given given view for a certain amount of time.
*
* #param view The view to iterate over and disable all clicks.
* #param millis The number of millis to disable clicks for.
*/
public static void disable(View view, Long millis) {
final List<View> clickableViews = (millis == null) ? null : new ArrayList<View>();
disableRecursive(view, clickableViews);
if (millis != null) {
MainThread.handler().postDelayed(new Runnable() {
#Override public void run() {
for (View v : clickableViews) {
v.setClickable(true);
}
}
}, millis);
}
}
private static void disableRecursive(View view, List<View> clickableViews) {
if (view.isClickable()) {
view.setClickable(false);
if (clickableViews != null)
clickableViews.add(view);
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); i++) {
disableRecursive(vg.getChildAt(i), clickableViews);
}
}
}
}

Bet my arm it's due to extra stateless fragments living somewhere inside of your app. I'd personally discourage retaining instance and let Android do what it can with it, while you use standard mechanism to keep your state (saveInstanceState, database, high-level classes/patterns, SharedPreferences, etc).
Personally I've had plenty of issues when retaining a fragment instance (normally when re-creating or re-activating fragments through config changes or leaving and re-entering the app), resulting generally on two fragments, one of them connected to views, stateless, thus useless; and the "real" one keeping the previous state without any connection to views, hence ending up with exceptions and all sort of fanciness you don't want to have.
Start by not retaining the instance and see what comes up.

You could use mHandler.removeCallbacksAndMessages(null) work in many situations for me.

Related

How to maintain Android GridView position in all cases?

I'm new to Android and I'm trying to do the following task on my school project:
I have a grid view of movies which the user can scroll endlessly.
When the app starts I fetch the first 20 movies and each time the user scrolls to the bottom of the grid I execute an AsyncTask to fetch 20 more movies and add them to the Adapter.
When the user clicks on a movie he goes to a new child activity to see the movie details.
I'm having troubles maintaining the GridView's scroll position in the following cases:
When the user goes to the details activity and returns to the main activity of the movies.
When the user changes the device orientation.
And when dealing with theses 2 cases I also need to take in consideration that maybe the user scrolled a lot and had 100 movies in the adapter and when he goes back the activity start from the start with only the first 20 movies, so I would be able to scroll to his last position.
Can someone please tell me how can I give the best user experience in my project by not losing the user's scroll position at any case?
I don't know if this is the best practice, but in my case it is.
I decided to set my adapter as a global static variable, in this way I maintain the amount of data loaded via the API, and I don't need to perform a request for every time the user moves between activities.
For maintaining the scroll position I used the onItemClickListener when moving to the details activity and the savedInstanceState when changing orientation.
Here is my code for that:
//Static variables
private static MoviesAdapter mMoviesAdapter;
private static int mGridViewPosition = 0;
//Call this method when user clicks the back button
public static void ClearStaticData(){
mMoviesAdapter.clear();
mMoviesAdapter = null;
}
#Override
public void onSaveInstanceState(Bundle outState) {
int index = mGridView.getFirstVisiblePosition();
outState.putInt(GRID_VIEW_POSITION, index);
super.onSaveInstanceState(outState);
}
#Override
public View onCreateView(...) {
if (mMoviesAdapter == null) {
mMoviesAdapter = new MoviesAdapter(...);
} else {
RestoreGridPosition();
}
}
private void RestoreGridPosition(){
if(mGridViewPosition > 0 && mMoviesAdapter.getCount() >= mGridViewPosition)
mGridView.setSelection(mGridViewPosition);
}
Since I fill my adapter via API call, I think this is probably the best solution to save the data and not to perform requests every time.
Try not finishing mainActivity once a gridItem is clicked so when user navigates back to mainActivity (from detailsActivity) he will have all the data that was there before.
You can handle this situation with activity's lifecycle callbacks:
You can get currently visible GridView item's position like this:
int mCurrentPosition = gridview.getFirstVisiblePosition();
When an orientation change is occurring the activity is recreated and going through the following stages:
onSaveInstanceState
onRestoreInstanceState
You can then save the position before orientation change is happening and get it back when its being restored.
Save Your Activity State
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current scroll state
savedInstanceState.putInt(STATE_POSITION, mCurrentPosition);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
Restore Your Activity State
public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance
mCurrentPosition = savedInstanceState.getInt(STATE_POSITION);
}
Here once you have the previous position you can move to the desired position in the gridView:
gridview.smoothScrollToPosition(int mCurrentPosition)
This is taken from android docs: Recreating an Activity
Scrolling gridView to position GridView scrolling stackoverflow

properly freeing resources in a view that belongs to an adapter

My list adapter creates some Views (both normal views and header views) that use constantly-running timers to cycle their displayed image.
I wonder what's the appropriate way to deallocate those timers and other view-related resources, i mean, the best practice to know that an Item View (created by Adapter.getView) is no more visible/needed, or has been recycled, or de-attached, or even their parent widget is not in use.
I'm ideally looking for a method / event / listener in the View itself, rather than the parent Widget having to delegate an event. I'd like to know
When the View is given to the recycler (ie- it has exited screen on a fling, etc)
When the View can be destroyed (ie- the containing widget has been destroyed)
So far I think I haven't found the appropriate, fail-proof event, if any.
I tried View.onAttachedToWindow() / View.onDetachedFromWindow() but, opposed to onAttachedToWindow that seems it's always properly called, onDetachedFromWindow is not. It seems it's only called when a recycled view is about to be reused, but not on other cases like when the View enters the recycling pool (exits screen), or when the widget is destroyed, or even when the activity is finished.
Any suggestion?
EDIT:
I have been doing some tests, and found that onDetachedFromWindow on the parent widget IS reliable. At the moment my solution is to propagate that event to the adapter, which in turn finds the views on screen (getFirstVisiblePosition...) and manually triggers an onDetachedFromWindow with the code below, but, you know, this is very ugly. I'm sure there's a better way.
// ugly function to trigger an onDetachedFromWindow to any view
// leeched from com.tonicartos.widget.stickygridheaders.StickyGridHeadersGridView
public static void detachHeader(View header) {
if (header == null) { return; }
try {
Method method = View.class.getDeclaredMethod("dispatchDetachedFromWindow");
method.setAccessible(true);
method.invoke(header);
} catch (NoSuchMethodException e) {
throw new RuntimePlatformSupportException(e);
} catch (IllegalArgumentException e) {
throw new RuntimePlatformSupportException(e);
} catch (IllegalAccessException e) {
throw new RuntimePlatformSupportException(e);
} catch (InvocationTargetException e) {
throw new RuntimePlatformSupportException(e);
}
}
RecyclerView.Adapter has a method to notify you when a view is recycled. Have a look at
public void onViewRecycled(RecyclerView.ViewHolder holder);
a) Leave the activity - flush everything in the adpater
May a proper way is to use the onStop Method of the surrounding activity. There you do have a reference to the adapter. In the adapter you can provide a flushAllResources() Method which you call from the activity.
This will work for cases where one leaves the activity.
#Override
public void onViewRecycled(#NonNull final MyViewHolder holder) {
flushAllViewRefreshHandlers();
}
b) View is gonna be re-used due to scrolling - flush the old data as the view was used previuously
Additionally in the onViewRecycled Method (as mentioned previously) you should also flush your resources.
This will work for cases where single items/views of the recycler are going to be re-used (recycled) for other entities. In this case you may only flush data of the specific view/entity and not all of the whole adapter.

Shared element activity transition on android 5

I wanted to setup a shared element transition when going from one Activity to another.
The first Activity has a RecyclerView with items. When an item is clicked that item should animate to the new activity.
So i've set a
android:transitionName="item" on both the final activity views, as wel as the recycler-view item views.
I'm also using this code when going to the next activity:
this.startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, itemView, "boomrang_item").toBundle());
When clicking an item, it transitions properly and the new view is shown. It is really nice.
However when i click the back button. Sometimes it works fine, but most of the time my activity crashes with the following stacktrace:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.ViewGroup.transformMatrixToGlobal(android.graphics.Matrix)' on a null object reference
at android.view.GhostView.calculateMatrix(GhostView.java:95)
at android.app.ActivityTransitionCoordinator$GhostViewListeners.onPreDraw(ActivityTransitionCoordinator.java:845)
at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:847)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1956)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1054)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5779)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767)
at android.view.Choreographer.doCallbacks(Choreographer.java:580)
at android.view.Choreographer.doFrame(Choreographer.java:550)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
What am i doing wrong?
It looks like a bug in android 5
I encounter the same issue, and notice the crash happens if the original shared element is no longer visible on the previous screen when you go back (probably it is the last element on screen in portrait, but once switched to landscape it's no longer visible), and thus the transition has nowhere to put back the shared element.
My workaround is to remove the return transition (in the 2nd activity) if the screen has been rotated before going back, but I'm sure there must be a better way to handle this:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mOrientationChanged = !mOrientationChanged;
}
#Override
public void supportFinishAfterTransition() {
if (mOrientationChanged) {
/**
* if orientation changed, finishing activity with shared element
* transition may cause NPE if the original element is not visible in the returned
* activity due to new orientation, we just finish without transition here
*/
finish();
} else {
super.supportFinishAfterTransition();
}
}
If you're using Proguard then try adding this into your rules file. I had the same issue and it appears to work?
-keep public class android.app.ActivityTransitionCoordinator
Try removing any merge xml tags that you might have on the final activity's view. I have noticed that transitioning to a view, that contains a merge tag, in which the transitioning element is a direct child of the merge tag, will cause this error, but should I replace the merge tag with a different container like CardView, the animation works just fine. Also make sure that there is a 1:1 relationship between the transitionNames in the views.
UPDATE:
I experienced this issue once more when doing an activity transition, clicking the back button to return to the initial activity, and then trying the transition again. I was accessing the direct parent of the 'transition component', (A RelativeLayout) by id, with a findViewById() call, and then calling removeAllViews(). I ended up changing the code to call 'removeAllViews()' on a greater ancestor than the parent, also removed a tag from the element that was to take the place of the 'transition component' after page load. This alleviated my issue.
Make sure the View you are Transitioning to in the Second Activity is not the root layout.
You can just wrap it in a FrameLayout with a transparent windowBackground.
I had this same issue, for me it was being caused by the recyclerview executing updates after/during the first exit transition. I think the shared element view was then sometimes getting recycled, meaning it would no longer be available for the transition animation, hence the crash (normally on the return transition but sometimes on the exit transition). I solved it by blocking updates if the activity is paused (used an isRunning flag) - note it was pausing but not stopping as it was still visible in the background. Additionally I blocked the update process if the transition was running. I found it enough to listen to this callback:
Transition sharedElementExitTransition = getWindow().getSharedElementExitTransition();
if (sharedElementExitTransition != null) {
sharedElementExitTransition.addListener(.....);
}
As a final measure, although i'm not sure if this made a difference, I also did recyclerView.setLayoutFrozen(true) / recyclerView.setLayoutFrozen(false) in the onTransitionStart / onTransitionEnd.
Be sure the "itemView" you are passing in the transition is the view clicked (received on your onClick() callback)
I have faced the same issue, actually I used firebase and I have list of information and when user tap it will call detailActivity with sharedAnimation in this activity I was updating it as seen using firebase so firebase event updating the list item as seen, in this case this problem is invoking because recycler view that screen layout was getting effected.
and it invoke an exception because that transition id which one we have passed it was no more, so I solve this issue using this method.
onPause() I have frozen the layout and onResume() set it as false;
#Override
public void onPause() {
super.onPause();
mRecycler.setLayoutFrozen(true);
}
#Override
public void onResume() {
super.onResume();
mRecycler.setLayoutFrozen(false);
}
And it's working.
What I came up with is to avoid transitioning back to Activity with RecyclerView, or changing back transition with something else.
Disable all return transitions:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public void finishAfterTransition() {
finish();
}
Or, if you want to disable only shared elements return transition, and be able to set your own return transition:
// Track if finishAfterTransition() was called
private boolean mFinishingAfterTransition;
#Override
protected void onCreate(#Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFinishingAfterTransition = false;
}
public boolean isFinishingAfterTransition() {
return mFinishingAfterTransition;
}
#Override
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void finishAfterTransition() {
mFinishingAfterTransition = true;
super.finishAfterTransition();
}
public void clearSharedElementsOnReturn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TransitionUtilsLollipop.clearSharedElementsOnReturn(this);
}
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static final class TransitionUtilsLollipop {
private TransitionUtilsLollipop() {
throw new UnsupportedOperationException();
}
static void clearSharedElementsOnReturn(#NonNull final BaseActivity activity) {
activity.setEnterSharedElementCallback(new SharedElementCallback() {
#Override
public void onMapSharedElements(final List<String> names,
final Map<String, View> sharedElements) {
super.onMapSharedElements(names, sharedElements);
if (activity.isFinishingAfterTransition()) {
names.clear();
sharedElements.clear();
}
}
});
}
With that implemented in base activity, you can easily use it in onCreate()
#Override
protected void onCreate(#Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
clearSharedElementsOnReturn(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// set your own transition
getWindow().setReturnTransition(new VerticalGateTransition());
}
}
I had this same error, mine was caused by the same reasoning behind hidro's answer but was caused by the keyboard hiding the shared element that the transition was going back to.
My workaround was to programmatically close the keyboard right before finishing the activity so the shared element on the previous activity isn't obscured.
View view = this.getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
supportFinishAfterTransition();
As #Fabio Rocha said, make sure that the itemView is retrieved from the ViewHolder.
You can get the ViewHolder by position via
mRecyclerView.findViewHolderForAdapterPosition(position);
The reason for this is actually quite simple:
When you Navigate back to the parent Activity or Fragment, the View is not there yet (could be for many reasons).
So, what you want to do is to postpone the Enter Transition until the View is available.
My work around is to call the following function in onCreate() in my Fragment (but works in Activity too):
private void checkBeforeTransition() {
// Postpone the transition until the window's decor view has
// finished its layout.
getActivity().supportPostponeEnterTransition();
final View decor = getActivity().getWindow().getDecorView();
decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
decor.getViewTreeObserver().removeOnPreDrawListener(this);
getActivity().supportStartPostponedEnterTransition();
return true;
}
});
}
Got same issue, and it caused by recycler view updating in background, the recycler view will recreate view when notifyItemChanged(int index), so the share view was recycled and it got crash when come back.
My solution is call recyclerView.setItemAnimator(null);, and it will prevent recycler view from recreating view.

Obtaining the current Android view and forcing it to be redrawn

How can I get the current Android view when it displays data that has been updated, and force it to be redrawn? I worked through Android's Notepad tutorial and completed lesson three without any problems — the solution is provided, after all — but I'm stuck on my first non-trivial modification.
I added a new button to the menu, next to the Add note button. When pressed, that button adds a letter to the title of each note in the system. However, the new titles don't show up in the list of notes no matter how long I wait. I know the updater works because the changes do appear if I dismiss the app and bring it back up.
So far, I've discovered that I have to use some kind of invalidation method to make the program redraw itself with the new values. I know that invalidate() is used from the UI thread and postInvalidate() is used from non-UI threads 1, 2, but I don't even know which thread I'm in. Also, both of those methods have to be called from the View object that needs drawing, and I don't know how to obtain that object. Everything I try returns null.
My main class:
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch(item.getItemId()) {
case INSERT_ID:
createNote();
return true;
case NEW_BUTTON:
expandTitles();
return true;
default:
// Intentionally empty
}
return super.onMenuItemSelected(featureId, item);
}
private void expandTitles() {
View noteListView = null;
// noteListView = findViewById(R.layout.notes_list); // null
// noteListView =
// getWindow().getDecorView().findViewById(android.R.id.content);
// From SO question 4486034
noteListView = findViewById(R.id.body); // Fails
mDbHelper.expandNoteTitles(noteListView);
}
My DAO class:
public void expandNoteTitles(View noteListView) {
Cursor notes = fetchAllNotes();
for(int i = 1; i <= notes.getCount(); i++) {
expandNoteTitle(i);
}
// NPE here when attempt to redraw is not commented out
noteListView.invalidate(); // Analogous to AWT's repaint(). Not working.
// noteListView.postInvalidate(); // Like repaint(). Not working.
}
public void expandNoteTitle(int i) {
Cursor note = fetchNote(i);
long rowId =
note.getLong(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_ROWID));
String title =
note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)) + "W";
String body =
note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY));
updateNote(rowId, title, body);
}
What do I have to do to get the updated note titles to show up as soon as I press the button?
Obviously, I'm a complete newbie to Android. I point this out to encourage you to use small words and explain even obvious things. I know this is the millionth "Android not redrawing" question, but I've read dozens of existing posts and they either don't apply or don't make sense to me.
1: What does postInvalidate() do?
2: What is the difference between Android's invalidate() and postInvalidate() methods?
According to the tutorial, the list of existing notes are presented in a ListView. That is an adapter based View, so the items it shows are sourced from an adapter extending theBaseAdapter class. In these cases, you should notify the adapter that the contents have changed by calling its notifyDatasetChanged method. This'll signal the ListView to update and redraw its rows.
Edit:
Sorry, I now realize that this example uses CursorAdapters. These source the items to show from a Cursor object that was obtained from a database query. Now, what the notifyDatasetChanged() tells the adapter is, that the data that backs the adapter has changed, so Views that show stuff based on this adapter need to redraw their contents. In the case of a CursorAdapter, this data is coming from a cursor. So you also need to requery that cursor, refreshing it from the DB, like this:
private void expandTitles() {
mDbHelper.expandNoteTitles();
CursorAdapter adapter = (CursorAdapter)getListAdapter();
adapter.getCursor().requery();
}
The requery() method automatically calls the notifyDatasetChanged() in this case, so you don't need to worry about that, the list will update itself. See this thread also: https://groups.google.com/forum/?fromgroups#!topic/android-developers/_FrDcy0KC-w%5B1-25%5D.

Activity lifecycle vs View lifecycle: how to avoid NPE?

I have a ListView which adapts data from a cursor. The cursor's position of the record is stored via View.setTag(), so it can be retrieved in response to user events.
public class OrderActivity extends Activity {
private ListView list;
private CursorAdapter adapter;
private SQLiteDatabase database;
public void onResume() {
database = new Database(this).getWriteableDatabase();
// query the db and store the position of the cursor
// as the tag while binding the view in the overridden
// CursorAdapter.bindView()
adapter = new SimpleCursorAdapter( /* code here */ );
list.setAdapter(adapter);
}
public void onPause() {
adapter.changeCursor(null);
database.close();
}
private void onClickRowDumpOrder(View row) {
int newPosition = (Integer) row.getTag();
Cursor cursor = adapter.getCursor();
int originalPosition = cursor.getPosition(); // Throws NPE
cursor.moveToPosition(newPosition);
Log.v("tag", "Description: " + cursor.getString(0));
// restore the cursor
cursor.moveToPosition(originalPosition);
}
}
I store the database and the adapter in instance fields of my Activity to allocate/free resources during the activity lifecycle: I create a new database onResume and close it onPause. Unfortunately, I receive lots of reports from my users that a NPE is thrown in the line outlined in the pseudocode above, but I'm not able to reproduce it.
It seems that cursor is null, but I wonder how this is possible if the method onClickRowDumpOrder can only be called after onResume, since it's the callback to the click event (I set this in the XML layout via android:onClick)
Am I doing something wrong? What API misusage causes the cursor to be null? Is there some documentation describing how cursors and adapters are intended to fit in the activity lifecycle?
UPDATE
I got rid of android:onClick in my XML file and manually set the listener inside SimpleCursorAdapter.bindView. To avoid leaks, I remove the listener either in my custom AbsListView.RecycleListener and in my activity's onPause (I retrieve all views with reclaimViews(List<View>)). This seems to fix the bug, and here is my explanation.
A new View is inflated when my activity first starts
instance#1 of my activity is set as the OnClickListener for that particular View in the View's constructor, when it parses the android:onClick attribute
instance#1 leaves the foreground, thus onPause() sets the cursor to null. Note that this activity is still the listener for the View, because neither the view nor the activity are marked for garbage collection. This means that they are referenced in some cache in the Android classes
instance#2 of my activity is created, and its list view somewhat recycle the already created View. The data is shown correctly, but this view still has the old activity (with a null cursor) as the listener
when user clicks my view, instance#1 handler is called, but it has a null cursor. This causes the NPE
This explanation is realistic, however I didn't find relevant code in Android classes (there is a cache in AbsListView.RecycleBin, but the ListView itself is not reused). Moreover, I've never been able to reproduce this bug, and I solely think my fix works because in the last two days I received no reports (usually I get a few dozens a day)
Are you aware of any code in the Android stack which can validate my assumptions?
you can try this way.
if (cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
}
}
I think you are creating the cursor at the wrong point in the Activity lifecycle. The ListView sample puts in onCreate() what you have in onResume(). I don't know that it's necessarily harmful, but you may be recreating some things that don't need to be.

Categories

Resources