Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fragment's OnClickListener called after onDestroyView

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.

like image 543
rockgecko Avatar asked Sep 03 '13 02:09

rockgecko


1 Answers

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

}
like image 67
spierce7 Avatar answered Oct 27 '22 00:10

spierce7