4

I have a Activity with ViewPager, In ViewPager Adapter I provide Fragment for each position.

An example Fragment is DebugFragment. I have written the source code below.

public class DebugFragment extends android.support.v4.app.Fragment {

private OnFragmentInteractionListener mListener;

public interface OnFragmentInteractionListener {
    void onFragmentInteraction(int someValue);
}

public static DebugFragment newInstance() {
    DebugFragment fragment = new DebugFragment();
    Bundle args = new Bundle();
    fragment.setArguments(args);
    return fragment;
}

private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        mListener.onFragmentInteraction(0);
    }
};

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    LocalBroadcastManager.getInstance(getContext()).registerReceiver(mMessageReceiver,
            new IntentFilter("com.android.example.INITIAL_REQUEST"));
}

@Override
public void onDestroy() {
    LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mMessageReceiver);
    super.onDestroy();
}

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    if (context instanceof OnFragmentInteractionListener) {
        mListener = (OnFragmentInteractionListener) context;
    } else {
        throw new RuntimeException(context.toString()
                + " must implement OnFragmentInteractionListener");
    }
}

@Override
public void onDetach() {
    super.onDetach();
    mListener = null;
}

@Override
public void onResume() {
    super.onResume();
    getUserData();
}

public void getUserData() {
 // Inside Background Thread
    if (getActivity() == null) {
        return;
    }
    getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
            mListener.onFragmentInteraction(0); // This line throws NPE
        }
    });
}

My Activity implementation below.

public class DebugActivity extends AppCompatActivity implements
    DebugFragment.OnFragmentInteractionListener {

    // Other Activity Callback

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE) {
            if (resultCode == Activity.RESULT_OK) {
                DebugFragment debugFragment = ((DebugFragment) mViewPagerAdapter.getRegisteredFragment(2));
                if (debugFragment != null) {
                    debugFragment.getUserData();
                }
            }
        }
    }
}

I call getUserData of my DebugFragment from onResume of Fragment, BroadcastReceiver, OnActivityResult of Activity.

Sometime I get NullPointerException in getUserData while trying to access FragmentListener i.e mListener. I want to know Why?

As i am already checking for Activity null. Isn't this enough. Do i have to check for null of mListener also? It would be great if somebody will explain me the case where activity will not be null but my mListener will be null. I have kept my activity in Portrait mode only.

Edit

My Adapter code

public abstract class TabPagerAdapter extends FragmentPagerAdapter {

    public TabPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    public abstract View getTabView(int position);
}

public class SecondaryPagerAdapter extends TabPagerAdapter {

    private static final int NUM_PAGES = 5;

    private String tabTitles[] = new String[] { "Today", "New", "Calendar", "In-progress", "Invoices" };
    private int[] imageResId = { R.drawable.ic_tab_hired_pro, R.drawable.ic_tab_history,
            R.drawable.ic_tab_today, R.drawable.ic_tab_inprogress, R.drawable.ic_tab_invoices };
    SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();

    public SecondaryPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
            case 1:
            case 3:
                return ServiceRequestFragment.newInstance(tabTitles[position]);
            case 2:
                return DebugFragment.newInstance();
            case 4:
                return InvoicesFragment.newInstance();
            default:
                throw new RuntimeException("No fragment for this position");
        }
    }

    @Override
    public int getCount() {
        return NUM_PAGES;
    }

    @Override
    public View getTabView(int position) {
        CustomTab customTab = new CustomTab(DashBoardActivity.this);
        customTab.bindWith(imageResId[position], tabTitles[position]);
        return customTab;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        registeredFragments.put(position, fragment);
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        registeredFragments.remove(position);
        super.destroyItem(container, position, object);
    }

    public Fragment getRegisteredFragment(int position) {
        return registeredFragments.get(position);
    }
}

From my Activity i call it like

mSecondaryPagerAdapter = new SecondaryPagerAdapter(getSupportFragmentManager());
mSecondaryPager = (ViewPager) findViewById(R.id.dashboard_pager);
mSecondaryPager.setOffscreenPageLimit(4);
mSecondaryPager.setAdapter(mSecondaryPagerAdapter);
shubendrak
  • 2,038
  • 4
  • 27
  • 56
  • How was your fragment created? Can you post the code? I suspect you have the NPE when your restart your App after your activity has been killed by the system. Is that the case? – Elye May 09 '16 at 13:58
  • @Elye I have added the code for fragment creation. I use static method NewInstance() from my viewPager adapter. I get crash report from user's device. I can't say exactly what the user have to cause this crash. What you have said may be the case. – shubendrak May 09 '16 at 14:06
  • Perhaps you could share your adapter's code? – Elye May 09 '16 at 14:09
  • @Elye I have added my Adapter code and how i use it from activity. – shubendrak May 09 '16 at 14:23
  • It looks okay to me. Didn't see any fault with that. You could check out http://stackoverflow.com/questions/7951730/viewpager-and-fragments-whats-the-right-way-to-store-fragments-state to see if you could spot anything. One option is to put a log out and see how many times a particular Fragment is instantiate again when the crash happens. If it is more than once, then perhaps you have 'recreated' a fragment while a 'savedInstance' fragment has been restored by the system... then the issue pretty much is along this line. – Elye May 09 '16 at 14:53
  • The other interesting bits is as noted in http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, onDestroy sometimes are not called... so you might not have unregistered your broadcast, which make it still listen to it perhaps, after your mListener = null is set during onDetech... But it's a common practice to unregister during onDestroy... so I'm not sure if this could be the cause. Do explore. Hopes you get some enlightenment... all the best. – Elye May 09 '16 at 14:56
  • Don't know who down vote your question, I think it's a valid question, I'll upvote it for you. – Elye May 09 '16 at 14:57
  • @fadden can you please take a look at this. – shubendrak May 09 '16 at 15:31
  • What is an example of where `getUserData()` might be called on a background thread as mentioned in the comment in the method? When in `onResume()`, `onActivityResult()`, and `onReceive()` of a `BroadcastReceiver` you are already on the UI Thread by default. Curious because if you are on the UI Thread then that means the line throwing the NPE runs synchronously. – George Mulligan May 11 '16 at 18:45
  • @GeorgeMulligan In getUserData() i send request to api and get result back in background thread, From background thread i update my UI by calling runOnUiThread. Note - I am not using AsyncTasK here. – shubendrak May 11 '16 at 20:07
  • So the posted `DebugFragment.getUserData()` method is not the full implementation? I do not see the API request. – George Mulligan May 11 '16 at 20:14
  • @GeorgeMulligan yes, getUserData() is not fully implemented. – shubendrak May 12 '16 at 09:30

4 Answers4

12

When you call getActivity().runOnUiThread(new Runnable() {}), this enqueues that Runnable to run on the UI thread after everything else that is already enqueued on the UI thread, which may include calls to onDestroy(), which would set mListener to null. This means that it's possible for your app to be closing while a broadcast comes in. While this shouldn't be a problem in the normal case because you unregister your receiver before clearing mListener, it is possible that the Runnable has been enqueued after this, which is why the listener is null when it executes.

To avoid the NPE, you should check for mListener == null in the Runnable. However, this still means that the callback is scheduled after your Fragment is destroyed, and because of that, your Fragment instance is leaked. The best thing to do is to create a Handler and post the Runnable to it instead of calling runOnUiThread(). Then, in onDestroy(), call mHandler.removeCallbacksAndMessages(null), which essentially clears the queue, so that the Runnable won't be called at all.

Jschools
  • 2,698
  • 1
  • 17
  • 18
2
mMessageReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      if(mListener != null){
        mListener.onFragmentInteraction(0);
      }
    }
};

When app receive broadcast, may be this activity has closed at that time

1

@Jschools has the best answer. But as an alternative you could also use Eventbus instead of the listener pattern. The eventbus will just broadcast the event and if the activity is not active no event will arrive and no NPE will be throwed.

jobbert
  • 3,297
  • 27
  • 43
0

As per documentation onAttach is called when a fragment is first attached to its context. Now since you are not directly instantiating the fragment inside the activity but through the Adapter, are you sure the onAttach(Context context) method of the DebugFragment is being called because that is where you have assigned the context to mListener?

There have been cases reported where onAttach(Context context) method is not called. Please see https://code.google.com/p/android/issues/detail?id=183358

You skip the Fragment lifecycle methods all together and set the mListener right in the newInstance().

public static DebugFragment newInstance(Context context) {
    DebugFragment fragment = new DebugFragment();
    fragment.setListener(context)
    Bundle args = new Bundle();
    fragment.setArguments(args);
    return fragment;
}

private void setListener(Context context){
if (context.instanceof(OnFragmentInteractionListener) {
        mListener = (OnFragmentInteractionListener) context;
     } 
     else {
        throw new RuntimeException(context.toString()
                + " must implement OnFragmentInteractionListener");
     }
}
Chebyr
  • 2,171
  • 1
  • 20
  • 30