Home » Android » android – Automatic UI configuration change handling in Activity and Fragment sometimes fails

android – Automatic UI configuration change handling in Activity and Fragment sometimes fails

Posted by: admin June 15, 2020 Leave a comment

Questions:

I have written android app now for a long time but now I’m facing a problem that I have never thought about. It is about the android lifecycle of Activitys and Fragments in in relation to configuration changes. For this I have create a small application with this necessary code:

public class MainActivity extends FragmentActivity {

    private final String TAG = "TestFragment";
    private TestFragment fragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FragmentManager fm = getSupportFragmentManager();
        fragment = (TestFragment) fm.findFragmentByTag(TAG);

        if (fragment == null) {
            fragment = new TestFragment();
            fm.beginTransaction().add(R.id.fragment_container, fragment, TAG).commit();
        }
    }
}

And here is my code for the TestFragment. Note that I’m calling setRetainInstance(true); in the onCreate method so the fragment is not recrated after a configuration change.

public class TestFragment extends Fragment implements View.OnClickListener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public View onCreateView(LayoutInflater li, ViewGroup parent, Bundle bundle) {
        View rootView = li.inflate(R.layout.fragment_test, parent, false);
        Button button = (Button) rootView.findViewById(R.id.toggleButton);

        button.setOnClickListener(this);
        return rootView;
    }

    @Override
    public void onClick(View v) {
        Button button = (Button) v;
        String enable = getString(R.string.enable);

        if(button.getText().toString().equals(enable)) {
            button.setText(getString(R.string.disable));
        } else {
            button.setText(enable);
        }
    }
}

And here is the layout that my fragment is using:

<LinearLayout
    ...>

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/toggleButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/enable"/>

</LinearLayout>

My problem is that if I rotate the device the text of the Button change back to the default value. Of course the View of the Fragment is new created and inflated but the saved instance for the Views should be restored. I also have an EditText in my layout and there the text and other properties remains after the rotation. So why is the Button not restore from the Bundle by default? I have read on the developer site:

By default, the system uses the Bundle instance state to save information about each View object in your activity layout (such as the text value entered into an EditText object). So, if your activity instance is destroyed and recreated, the state of the layout is restored to its previous state with no code required by you.

I’ve also read a lot of answers the last days but I do not know how actual they are anymore. Please do not leave a comment or an answer with android:configChanges=... this is very bad practice. I hope someone can bring light into my lack of understanding.

How to&Answers:

You should save state of your fragment in the onSaveInstanceState(Bundle outState) and restore it in the onViewCreated(View view, Bundle savedState) method. This way you will end up with the UI just as it was before configuration change.

Answer:

TextView subclasses don’t save their text by default. You need to enable freezesText="true" in the layout, or setFreezesText(true) at runtime for it to save its state.

Answer:

As per documentation, views should maintain their state without using setRetainInstance(true). Try to remove it from your onCreate, this should force the fragment to be recreated on screen rotation, hence all of it’s views should be saved before rotation and restored after.

Answer:

This stack overflow should answer your question:
setRetainInstance not retaining the instance

setRetainInstance does tell the fragment to save all of its data, and for ui elements where the user has manipulated the state (EditText, ScrollView, ListView, etc) the state gets restored. That being said, normal read-only UI components get reinflated from scratch in onCreateView and have to be set again – my guess would be that their properties are not considered “data” that needs to be retained and restored – Google probably does this for performance reasons. So things like a normal Button, ImageView, or TextView need their contents set manually when they are reinflated if it differs from the initial state in the XML. (TextView’s android:freezesText basically puts the TextView in a mode that uses an implementation to save it’s state and restore it).

PS: According to this stack overflow Save and restore a ButtonText when screen orientation is switched, you may be able to set android:freezesText on the button to have it keep the text you set – I haven’t tried it, but it makes sense.

Answer:

Edit after Op feedback

Although this fails to answer the question, after consultation with the OP, I’ve decided to leave it here as a reference point of information on the topic for people who land here. Hope you find it helpful.


Try putting your setRetainInstance in onCreateView. See here

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // setRetainInstance(true);
}

@Override
public View onCreateView(LayoutInflater li, ViewGroup parent, Bundle bundle) {
    setRetainInstance(true);
    View rootView = li.inflate(R.layout.fragment_test, parent, false);
    Button button = (Button) rootView.findViewById(R.id.toggleButton);

    button.setOnClickListener(this);
    return rootView;
}

Called when the fragment’s activity has been created and this
fragment’s view hierarchy instantiated. It can be used to do final
initialization once these pieces are in place, such as retrieving
views or restoring state. It is also useful for fragments that use
setRetainInstance(boolean) to retain their instance, as this
callback tells the fragment when it is fully associated with the new
activity instance. This is called after onCreateView(LayoutInflater,
ViewGroup, Bundle) and before onViewStateRestored(Bundle).

developer.android.com/reference/android/app/Fragment.html#onActivityCreated

Control whether a fragment instance is retained across Activity
re-creation (such as from a configuration change). This can only be
used with fragments not in the back stack. If set, the fragment
lifecycle will be slightly different when an activity is recreated:

onDestroy() will not be called (but onDetach() still will be, because
the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being
re-created.
onAttach(Activity) and onActivityCreated(Bundle) will
still be called.

developer.android.com/reference/android/app/Fragment.html#setRetainInstance

And taken from here:

onCreate : It is called on initial creation of the fragment. You do
your non graphical initializations here. It finishes even before the
layout is inflated and the fragment is visible.

onCreateView : It is called to inflate the layout of the fragment i.e
graphical initialization usually takes place here. It is always called
sometimes after the onCreate method.

onActivityCreated : If your view is static, then moving any code to
the onActivityCreated method is not necessary. But when you – for
instance, fill some lists from the adapter, then you should do it in
the onActivityCreated method as well as restoring the view state when
setRetainInstance used to do so. Also accessing the view hierarchy of
the parent activity must be done in the onActivityCreated, not sooner.

Let me know if this helps.