onClickListener not working (properly) after ConfigurationChange in fragments

1k views Asked by At

Introduction

Huh, this is a tough one. At least, I think so... To clear things up:

I did not find any answers to my question after searching the internet (using Google).

Everything I found was about people setting up the onClickListener's for their View's wrong. I guess it is the same problem in my case, but none of the other problems matched mine.

This seems to be the same problem... It has no answers.

Setup

I've got three Fragment's set up together with a ViewPager in my AppCompatActivity.

viewPager = (ViewPager) findViewById(R.id.main_view_pager);
viewPager.setAdapter(new SectionsPagerAdapter(getSupportFragmentManager()));

Handled by the SectionsPagerAdapter.

public class SectionsPagerAdapter extends FragmentPagerAdapter {

    SectionsPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return MainTextHolder.newInstance(tabIndicatorOnClick);
            case 1:
                return PostWriter.newInstance(tabIndicatorOnClick);
            case 2:
                return TopPage.newInstance(tabIndicatorOnClick);
            default:
                return Error.newInstance();
        }
    }

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

    // ... more methods
}

In each of the Fragment's I have some content plus a custom TabIndicator.

(the following xml file is my View's for the indicator)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <View
        android:id="@+id/fragment_divider_one"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:tag="one" />

    <View
        android:id="@+id/fragment_divider_two"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:tag="two" />

    <View
        android:id="@+id/fragment_divider_three"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:tag="three" />

</LinearLayout>

And then I set an OnClickListener for each of those View's (dividers) in the Fragment's onCreateView(LayoutInflater inflater, ViewGroup Bundle savedInstanceState) method. The OnClickListener is already prepared in my Activity where I also instantiate my ViewPager so that I can change the current Item (Fragment/tab).

private final View.OnClickListener tabIndicatorOnClick = new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (view.getTag().toString().equals("one"))
            viewPager.setCurrentItem(0, true); // second argument for smooth transition
        else if (view.getTag().toString().equals("two"))
            viewPager.setCurrentItem(1, true);
        else if (view.getTag().toString().equals("three"))
            viewPager.setCurrentItem(2, true);
    }
};

I pass that OnClickListener to the Fragment's by putting it into my newInstance(View.OnClickListener tabIndicatorOnClick) method.

This is for one of the fragments. It is identical for the others!

public static MainTextHolder newInstance(View.OnClickListener tabIndicatorOnClick) {
    MainTextHolder fragment = new MainTextHolder();
    Bundle args = new Bundle();
    fragment.putIndicatorTabIndicatorOnClick(tabIndicatorOnClick);
    fragment.setArguments(args);
    fragment.setRetainInstance(true);
    return fragment;
}

My putIndicatorTabIndicatorOnClick(View.OnClickListener tabIndicatorOnClick) method is a Void in an Interface. It just applies the OnClickListener to the Class(Fragment).

@Override
public void putIndicatorTabIndicatorOnClick(View.OnClickListener tabIndicatorOnClick) {
    this.tabIndicatorOnClick = tabIndicatorOnClick;
}

Does it work

Yes it does. It works perfectly fine... until a ConfigurationChange happens. In my case I tested it with changing the orientation.

The Problem

What happens after that ConfigurationChange is that everything goes normally. The OnClickListener gets applied to all View's in all Fragment's, but the tabIndicatorOnClick OnClickListener is a null object reference in the second and third Fragment.

So in PostWriter and TopPage there isn't even really an OnClickListener on the View's for what ever reason. But it gets better: In MainTextHolder Fragment the tabIndicatorOnClick is not a null object reference, but it does not change the ViewPager's Item anymore. It runs the code, but it does not scroll the Tab.

When turning on "Don't keep activities" ind Developer options of the device and leaving the app, all OnClickListener's are null object references. Even those in MainTextHolder.

Summary

After the ConfigurationChange the OnClickListener gets passed into my first Fragment, but it is no working properly and in the remaining Fragment's it is not even passed / a null object reference.

The issue can be replicated through destroying the Activity and then reloading it.

In the end...

... I have no clue what is going wrong. I hope that I structured my question properly.

May be of interest

minSdkVersion: 14

targetSdkVersion: 25

I already have a SpringIndicator on my ViewPager. This does not feature an OnClickListener so I added my own "overlayed" indicators which have the needed OnClick feature. I also like the look of it, but if you can give me a better solution for my TabIndicator, I would also love to have that as an answer.

I came across the solution to put the method into the xml :onClick attribute multiple times, but for that I would need to create the same OnClickListener for every Fragment which is not really the nice way and it is also not solving my problem, but a way around it. - I would need to pass the ViewPager into my Fragment and then call that method from xml which is, as I said, just another way of doing it and not the way I prefer to do it. Also I don't know if it works. I'll most likely test it.

Other OnClickListener's in the Fragment still work properly. So as I said the problem lays in the OnClickListener itself not working right/being a null object reference.

1

There are 1 answers

3
David Rawson On BEST ANSWER

Firstly, well done for writing a detailed description of the problem. Many questioners on StackOverflow can learn from the ability of this question to articulate the problem.

This error would seem to come from not accounting for Fragment lifecycle changes. While inside a ViewPager, Fragments will pass through various states including hidden and shown when they are temporarily offscreen, and paused and resumed if they are offscreen and the system frees memory, and finally created and destroyed if the Android system decides to save the instance state of your Activity (such as from a configuration change). In the latter case, Android will try and restore the state of your Fragments from saved instance state in your ViewPager. Since your Fragments have no way to save the View.OnClickedListener they were passed in the arguments, they end up with a null pointer when they are restored which is causing your error.

To fix the error, I suggest that you do not pass the View.OnClickedListener as a parameter to the Fragments. Rather, expose the OnClickListener to your Fragments via a public method in your Activity and have the Fragments get it themselves in their onResume(). This way you can guarantee that the Fragments will have a reference to the View.OnClickedListener whenever they are in a resumed state. So your onResume() might look something like this:

@Override
public void onResume() {
    OnClickListener onClickListener = ((MyActivity) getActivity).getOnClickListener();
    putIndicatorTabIndicatorOnClick(onClickListener);

}

Since you have set it in onResume(), you should remove it in onPause():

@Override
public void onPause() {
    //set the onClickListener to null to free up the resource
}

Using onResume() and onPause() like this to free up resources from listeners is the approach recommended in the developer guide.

Of course, you will have to make sure your Activity also handles its own lifecycle by checking for saved state in onCreate(Bundle savedInstanceState) and restoring it appropriately.

Please note that saving instance state and restoring can be triggered in a number of ways:

  1. Configuration state change (such as rotating the phone and causing the Activity to display in landscape rather than portrait
  2. The device becoming low on memory (for example, if a large number of Activities are in recent apps)
  3. Turning on Developer Options / Don't keep activities and using home to put your application in recent apps and then navigating back to your app.

You will be able to use one of these methods to determine if you have correctly handled the lifecycles of your Activities and Fragments.