11

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.

Community
  • 1
  • 1
rockgecko
  • 4,127
  • 2
  • 18
  • 31
  • can you give the fragment code ? – Ratul Ghosh Jul 18 '14 at 10:42
  • 1
    Can you post your onclick code? are you doing any background work in there? Are you checking if the getView() == null in the onclick? – user1634451 Dec 03 '14 at 00:00
  • does your app support rotations? When your phone rotates the current activity is destroyed and a new one is created. This means if the user clicked on an item just before rotation then the click will come back but the Activity context and view will be null. As suggested check that the view is not null, make sure you are not simulating trying to access context across a rotation (because it is destroyed). – toidiu Dec 03 '14 at 05:05
  • Are you using a static inner class for the OnClickListener? What I'd do as a quick fix is override onBackPressed() and set Clickable to false on all the elements or put in a state boolean where you don't do anything if back has been pressed. Keep in mind that the ClickListener has an implicit reference to the element it listens to so it will still be referencing the object even if you setOnItemClickListener(null). It could even be that the OnDestroy is delayed enough to get called before the set null happens. – G_V Dec 12 '14 at 08:47
  • possible duplicate of [Fragment's getView() returning null in a OnClickListener callback](http://stackoverflow.com/questions/17785388/fragments-getview-returning-null-in-a-onclicklistener-callback) – corsair992 Dec 16 '14 at 10:32

3 Answers3

1

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:

  1. getSupportFragmentManager().executePendingTransactions() This will execute all pending transactions right then, and removes the pending aspect

  2. 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)

  3. 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);
      }
    }
  }

}
spierce7
  • 14,797
  • 13
  • 65
  • 106
  • This issue is actually unrelated to `Fragment` transactions - it's caused by a bug in pre-ICS(/Honeycomb?) implementations where the callback for performing clicks was retained in the message queue when the `View` was detached from the `Window`. Making the `View`s unclickable upon detachment will not solve this issue, as any callbacks that are already in the queue will still be performed - you need to actually remove all the click listeners to solve this issue. I have added an answer to that effect on the other linked [question](http://stackoverflow.com/q/17785388/3153792). – corsair992 Dec 16 '14 at 07:00
  • @corsair992 Do you have a link to the issue tracker for this defect? I am investigating the possibility that something like this may be happening on newer devices. Thanks! – Shawn May 31 '17 at 21:44
  • @FearTheCron: As I mentioned in my [answer](https://stackoverflow.com/a/27497711) to the linked question, I am not aware of any old bug reports on this issue. This was just something I discovered while looking through the Android source code to find a potential cause for the reported issues. – corsair992 Jun 01 '17 at 05:06
-1

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.

Jose L Ugia
  • 5,960
  • 3
  • 23
  • 26
-1

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

Kevin Crain
  • 1,905
  • 1
  • 19
  • 28