1

I have have a FragmentActivity with two tabs which are ListFragments. Each ListFragment has a callback.

(Note: In case it's noticed, most of this question is rehashed from my own question Callback after orientation change becomes null, but this is a different question since this isn't related to rotation, but my hopes of understanding the actual problems with my callbacks.)

An example of a callback

The callback is associated inside of the onAttach(...) method as described by http://developer.android.com/training/basics/fragments/communicating.html

OnTab1Listener mTab1Callback;

public interface OnTab1Listener {
    public void onTab1Update();
}

@Override
public void onAttach(Activity activity) {
    Log.d(TAG, "onAttach");
    super.onAttach(activity);

    try {
        mTab1Callback = (OnTab1Listener)activity;
    } catch (ClassCastException e) {
        throw new ClassCastException(activity.toString() + " must implement OnTab1Listener");
    }
}

Later on, I communicate with the FragmentActivity by this callback which works fine normally.

After the application is sent to the background for a while and I bring the application to the front and trigger something that will use the callback, the callback is null and my app crashes. I believe this happens once it's in the background and the system does garbage collection.

Here is an example call which works fine during normal execution.

public void toggleEnabled(View v) {
    Log.d(TAG, "toggleEnabled");

    // null pointer here
    mTab1Callback.onTab1Update();
}

Question

  1. Why is mTab1Callback garbage collected and NOT restored if things are garbage collected? My impression was that the Activity lifecycle and Fragment lifecycle would restart, including onAttach(...) if part of the references are garbage collected.
  2. How is this properly fixed?

Example App

I've created an example application that exhibits this behavior. The full source code is here http://www.2shared.com/file/kkgFejUq/broken_example.html

Example App Stack Trace

FATAL EXCEPTION: main
java.lang.IllegalStateException: Could not execute method of the activity
at android.view.View$1.onClick(View.java:3591)
at android.view.View.performClick(View.java:4084)
at android.widget.CompoundButton.performClick(CompoundButton.java:100)
at android.view.View$PerformClick.run(View.java:16966)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
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:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at android.view.View$1.onClick(View.java:3586)
... 12 more
Caused by: java.lang.NullPointerException
at com.example.myapp.Tab2.toggleEnabled(Tab1.java:87)
at com.example.myapp.MainActivity.onClick(MainActivity.java:50)
... 15 more

Here is Tab1.java:87

mTab1Callback.onTab1Update();

This becomes a null pointer if you let the app go to the background, do other stuff for a while such as play a game for a bit, music, etc and return it to the foreground and click one of the check boxes.

In case it matters, I place the app icon on the launcher and launch it from there and then bring it to the foreground after I've run the other apps for a while. This is when it crashes.

Also I can't get it to crash on an emulator, only real hardware. It seems less likely to crash when the phone is plugged into the computer and easier if it's unplugged while testing.

Does the TabsAdapter have anything to do with this?

Also, this uses SherlockActionBar which is included in the file. It should be used as a library of MyApp.

Update

I began to see where getActivity() is null and after rotation or running in background long term, the only place it is null is inside the toggleEnabled() function that fires the callback.

This seem to point to the culprit being the MainActivity referencing a Fragment that no longer exists.

Combining code for space, this is essentially how I'm referencing the toggleEnabled() function after I click a CheckBox in Tab1. (see attached example for full code)

public void onClick(View v) {
     Tab1 tab = (Tab1)mTabsAdapter.getItem(0);
     tab.toggleEnabled(v);
}

So I dug into TabsAdapter to follow getItem(...). Here is what is there

private ArrayList mFragments = new ArrayList(); // code to setup tabs, etc @Override public Fragment getItem(int position) { Fragment frag = mFragments.get(position); if (frag.getActivity() == null) Log.d(TAG, "getItem: Activity is null");

return mFragments.get(position);

}

What happens is before rotation, getActivity() is not null. Once it is rotated, getActivity() is null. This seems to indicate that it's not recreating the Fragment with regard to the proper Context I believe. However this is where my knowledge of this sort of thing ends.

I can make mFragments static and it works, but I feel like that isn't a correct solution.

Does anyone know why the Fragments don't have a properly attached Activity?

Community
  • 1
  • 1
Kirk
  • 16,182
  • 20
  • 80
  • 112
  • Can you share a sample application that shows this behavior? – user Dec 16 '12 at 06:25
  • I've added an example app and a stacktrace of that app. – Kirk Dec 16 '12 at 22:35
  • I didn't see that exception. Lose the `setRetainInstance(true);` call in your fragments(why did you use it?) and see if you see the behavior again. – user Dec 17 '12 at 15:22
  • I'll give it a try. The reason I used it was in another `Activity` / `Fragment` combo that has a number of UI elements and I was using to retain their state. I suppose it's not necessary in the `Fragment`s I'm experiencing in this part of the app – Kirk Dec 17 '12 at 15:34
  • You would not need to retain the instance especially as you use the fragments in the `ViewPager` and you only have two fragments so they will both be available. Even so your code should work because the `onAttach` callback will still be called even if you retain the instance. – user Dec 17 '12 at 15:42
  • This seems to go back to the original issue I was having a while back. If I remove `setRetainInstance`, rotate the phone and then check / uncheck one of the `CheckBox`es, it will crash with a null pointer to the `mTab1Callback.onTab1Update()`. I believe I have something basic incorrect here. This happens in the test app I posted. – Kirk Dec 17 '12 at 15:44

2 Answers2

1

OnAttach looks to be called in a similar way to onCreate i.e. only once when created so when you resume from the background this method is skipped

MrChaz
  • 1,065
  • 1
  • 8
  • 17
  • Any idea on how to properly fix this? – Kirk Dec 16 '12 at 22:44
  • I would probably move the code from onAttach to onResume in the fragment – MrChaz Dec 17 '12 at 11:55
  • I believe there is something else wrong with the application causing this. As far as I can tell, onAttach is the correct place but something is interfering with proper references. – Kirk Dec 17 '12 at 15:35
  • Thank you for your help. I tested with `onResume` and it just crashed. – Kirk Dec 17 '12 at 15:38
1

Does anyone know why the Fragments don't have a properly attached Activity?

It's because the way the ViewPager works. When you first setup the ViewPager(along with instantiating the two fragments) it will call the getItem() method and it will receive the correct Fragment. Once you rotate the phone the Activity along with all the Fragments in it will be destroyed and recreated again. At this moment, the ViewPager will try to recreate it's state internally and will make the Fragments to match the previous state and it will construct his own Fragments. Your own Fragments that you add with the addTab method will not be used because after rotating the phone the getItem() method will not be called for the initial Fragments. The problem is that with your current code you call the getItem method to get the Fragments of the ViewPager which will retrieve the Fragments from the list. Those fragments are simple instances which aren't tied to an Activity so the callback set in the onAttach() method is null.

The main problem is that getItem can't be called on a FragmentPagerAdapter to get the Fragment for that position. There are a lot of question on stackoverflow on how to do this. I've written the method below to always have in the fragment list the correct fragment, no matter what the ViewPager does(by dropping the fragments you set for example). Your current code will remain the same just add:

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment created = (Fragment) super
            .instantiateItem(container, position);
    if (position < mFragments.size()) {
        Fragment fromList = mFragments.get(position);           
        if (!created.equals(fromList)) {
            mFragments.set(position, created);
        }
    } else {
        int difference = position - mFragments.size();
        if (difference == 0) {
            mFragments.add(created);                
        } else {
            for (int i = 0; i < difference - 1; i++) {
                mFragments.add(null);
            }
            mFragments.add(created);
        }
    }
    return created;
}

to the TabsAdapter. It should drop any manually created Fragments favoring the ViewPager's own fragments. see if it works.

Also:

  • Don't use the setRetainInstance(true)
  • Be careful when using static fields which have a reference to the Context to avoid memory leaks(for example, your MyObjectAdapter should be made an instance field and not a static one).
user
  • 86,916
  • 18
  • 197
  • 190
  • Well that fixed it. You did mention that I shouldn't use `getItem` in the first place. I'll look through StackOverflow for other examples, but do you have any specific method to use or is the addition you provided correct enough? – Kirk Dec 17 '12 at 19:29
  • @Kirk If you don't mind the hackish nature , you could use this `getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.pager + ":0")` to get the fragment at position `0`. My code should work, but I don't have where to test it now. – user Dec 17 '12 at 19:35
  • Ah, I prefer the way in your solution at this point. This has been a long journey for me to solve this issue so this is amazing. If someone is nearby, please tell them to give you a high-five. – Kirk Dec 17 '12 at 19:39