Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - How to stop fragment from automatically restoring its state?

I have created a whole new project to demonstrate my problem. Basically, my fragment has two RadioButton. When onCreateView, I always use radioButtonFirst.setChecked(true);. But if I check the radioButtonSecond then navigate out by pressing Back button, next time opening it, it automatically checks the radioButtonSecond. That behavior is breaking my logic.

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private WeirdFragment weirdFragment;
    private FragmentManager fragmentManager;
    private FragmentTransaction fragmentTransaction;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        weirdFragment = new WeirdFragment();
        fragmentManager = getSupportFragmentManager();
    }
    public void openFragment(View v){
        fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.container, weirdFragment);
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
    }
}

WeirdFragment.java

public class WeirdFragment extends Fragment {
    private RadioButton radioButtonFirst;
    private RadioButton radioButtonSecond;
    private Button buttonReturn;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_weird, container, false);
        radioButtonFirst = view.findViewById(R.id.radio_button_first);
        radioButtonSecond = view.findViewById(R.id.radio_button_second);
        buttonReturn = view.findViewById(R.id.button_return);

        radioButtonFirst.setChecked(true);

        // I decide to set a listener right here, to see what happened to my radio button. So this listener will be removed in my real project.
        radioButtonSecond.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                Log.v("weird", "i got here");
            }
        });
        buttonReturn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getFragmentManager().beginTransaction().remove(WeirdFragment.this).commit();
            }
        });
        return view;
    }
}

activity_main.xml

<Button
    android:id="@+id/button_open"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="openFragment"
    android:text="Open"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toBottomOf="@id/button_open" />

fragment_weird.xml

<RadioGroup
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintTop_toTopOf="parent">

    <RadioButton
        android:id="@+id/radio_button_first"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:checked="true"
        android:text="First" />

    <RadioButton
        android:id="@+id/radio_button_second"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Second" />
</RadioGroup>

<Button
    android:id="@+id/button_return"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Return"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

I set a breakpoint at the log statement. That breakpoint, when triggered by user action, the stacktrace will look like this:

onCheckedChanged:31, WeirdFragment$1 (com.peanut.myweirdradiobutton)
setChecked:154, CompoundButton (android.widget)
toggle:113, CompoundButton (android.widget)
toggle:78, RadioButton (android.widget)
performClick:118, CompoundButton (android.widget)
run:19866, View$PerformClick (android.view)
handleCallback:739, Handler (android.os)
dispatchMessage:95, Handler (android.os)
loop:135, Looper (android.os)
main:5254, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
invoke:372, Method (java.lang.reflect)
run:903, ZygoteInit$MethodAndArgsCaller (com.android.internal.os)
main:698, ZygoteInit (com.android.internal.os)

But when that breakpoint is triggered from out of nowhere, the stracktrace looks like this:

onCheckedChanged:31, WeirdFragment$1 (com.peanut.myweirdradiobutton)
setChecked:154, CompoundButton (android.widget)
onRestoreInstanceState:522, CompoundButton (android.widget)
dispatchRestoreInstanceState:13740, View (android.view)
dispatchRestoreInstanceState:2893, ViewGroup (android.view)
dispatchRestoreInstanceState:2893, ViewGroup (android.view)
restoreHierarchyState:13718, View (android.view)
restoreViewState:494, Fragment (android.support.v4.app)
moveToState:1486, FragmentManagerImpl (android.support.v4.app)
moveFragmentToExpectedState:1784, FragmentManagerImpl (android.support.v4.app)
moveToState:1852, FragmentManagerImpl (android.support.v4.app)
executeOps:802, BackStackRecord (android.support.v4.app)
executeOps:2625, FragmentManagerImpl (android.support.v4.app)
executeOpsTogether:2411, FragmentManagerImpl (android.support.v4.app)
removeRedundantOperationsAndExecute:2366, FragmentManagerImpl (android.support.v4.app)
execPendingActions:2273, FragmentManagerImpl (android.support.v4.app)
run:733, FragmentManagerImpl$1 (android.support.v4.app)
handleCallback:739, Handler (android.os)
dispatchMessage:95, Handler (android.os)
loop:135, Looper (android.os)
main:5254, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
invoke:372, Method (java.lang.reflect)
run:903, ZygoteInit$MethodAndArgsCaller (com.android.internal.os)
main:698, ZygoteInit (com.android.internal.os)

So I can see that it is the onRestoreInstanceState checking my radioButtonSecond. But I don't know how to prevent it. Things I am considering:

  1. Doing my logic in onViewStateRestored. But that is rather a not clean solution.
  2. Re-create my fragment everytime I need to open it. But my initializing phase when creating a new fragment is costly, I shouldn't do that.

In my real project, this fragment is like a setting for something in my app, it has a Cancel and an OK button. So, I want the fragment is in its default state every time it is opened because I think, for example, the user might configure many many things, but then press Cancel, then it is reasonable to see all of the views are in their default states, rather than the configured one (maybe this thought is causing my problem, is it a good user experience?)

So, how to prevent my fragment from restoring its state without my permission?

like image 652
peanut Avatar asked Nov 07 '22 17:11

peanut


1 Answers

If I understand you correctly, you want to reset the state of the RadioButtons before the users are leaving the Fragment in order to prepare for the case that the Fragment is reentered at some later time.

Since you want to have the first RadioButton checked each time the users reenter the Fragment after leaving it but (I suppose) not when the users just left your app for half an hour and then returned to continue right where they left off, you should use the savedInstanceState Bundle to make sure the RadioButtons have the correct state depending on the situation:

Let's introduce a field private boolean isFirstButtonChecked = true;.

Keep the OnCheckedChangeListener to track the checked state of the RadioButtons using

isFirstButtonChecked = radioButtonFirst.isChecked();

In addition to that, set the boolean to true in the OnClickListener

buttonReturn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            isFirstButtonChecked = true;
            getFragmentManager().beginTransaction().remove(WeirdFragment.this).commit();
        }
});

Now you can save the desired state in onSaveInstanceState():

     @Override
public void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean("FIRST_RB_STATE", isFirstButtonChecked);
}

@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);
    isFirstButtonChecked = savedInstanceState == null 
            ? true 
            : savedInstanceState.getBoolean("FIRST_RB_STATE");
    radioButtonFirst.setChecked(isFirstButtonChecked);
}

I'm aware that this means adding quite a lot of code. But since you are reluctant to recreate your Fragment from scratch each time I suppose it contains more than just two RadioButtons. So letting the runtime work for you instead of struggling against it makes this approach cleaner and safer than just calling super.onViewStateRestored(null); in the corresponding method.

like image 110
Bö macht Blau Avatar answered Nov 11 '22 11:11

Bö macht Blau