2

I have put together a simple program that uses fragment replacement with a single activity and two fragments. One fragment has a button, that when pressed, replaces the fragment with a second fragment.

This works as expected if the application is started and the button is clicked. First Fragment Fragment replacement success

If the application is started, the device is rotated (either to landscape or continuing back to portrait), and the button is clicked, then the fragment replacement fails with both fragments being displayed simultaneously. Fragment replacement failure

What is going on here? Is there fragment lifecycle issue I am failing to address?

MainActivity.java

package edu.mindlab.fragmenttest;

import android.app.FragmentTransaction;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;


public class MainActivity extends ActionBarActivity{
    private MainActivityFragment startingFragment;
    private ReplacementFragment replacementFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startingFragment = new MainActivityFragment();
        replacementFragment = new ReplacementFragment();

        FragmentTransaction t = getFragmentManager().beginTransaction();
        t.add(R.id.fragment_container, startingFragment, "start").commit();
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public void replaceFragment(View view){
        if(!replacementFragment.isAdded()) {
            FragmentTransaction t = getFragmentManager().beginTransaction();
            t.replace(R.id.fragment_container, replacementFragment, "replacementTest").commit();
        }
    }
}

MainActivityFragment.java

package edu.mindlab.fragmenttest;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MainActivityFragment extends Fragment {
    public MainActivityFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);

        return view;
    }
}

ReplacementFragment.java

package edu.mindlab.fragmenttest;

import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class ReplacementFragment extends Fragment {

    public static ReplacementFragment newInstance() {
        ReplacementFragment fragment = new ReplacementFragment();
        return fragment;
    }

    public ReplacementFragment() {
        // Required empty public constructor
    }

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

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_replacement, container, false);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
    }

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

}

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:id="@+id/fragment_container">
</FrameLayout>

fragment_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingBottom="@dimen/activity_vertical_margin"
            tools:context=".MainActivityFragment">

<TextView
    android:text="@string/start_string"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/textView"/>

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/replacement_fragment"
    android:id="@+id/button"
    android:layout_below="@+id/textView"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_marginTop="56dp"
    android:onClick="replaceFragment"/>


</RelativeLayout>

fragment_replacement.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context="edu.mindlab.fragmenttest.ReplacementFragment">

<TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="@string/replacement_fragment"/>

</FrameLayout>
mattm
  • 5,851
  • 11
  • 47
  • 77

2 Answers2

4

The answer is simple. UI state is retained across screen rotations meaning if you do nothing the screen would still show the second fragment once you rotate. Since you add the first fragment in your Activity onCreate method it will be added to what is already shown hence the two fragments overlap.

You could do something like this:

FragmentManager mgr = getFragmentManager();
if (mgr.findFragmentByTag("start") == null &&
    mgr.findFragmentByTag("replacementTest") == null) {

    FragmentTransaction t = mgr.beginTransaction();
    t.add(R.id.fragment_container, startingFragment, "start").commit();             

}
Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85
  • This explains to me why both fragments show if the screen is rotated after the fragment has been replaced. I do not understand, however, why replace fails when called after rotation. Based on the documentation, I think replace should remove all fragments and replace them with the argument. "Replace an existing fragment that was added to a container. This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here." – mattm Jun 05 '15 at 17:44
  • Same reason. When you rotate another first fragment is added but you don't see that because they look identical. When you replace, only one them is replaced by the second fragment and then you have the same overlap. – Emanuel Moecklin Jun 05 '15 at 18:13
  • So you are saying that the documentation for replace is wrong? It does not remove all currently added fragments? This would be consistent with the behavior I see. – mattm Jun 05 '15 at 18:56
  • No that's not what I'm saying. I'm saying that you are replacing but since there are two MainActivityFragment added after a screen rotation it will replace just one of them – Emanuel Moecklin Jun 05 '15 at 19:07
  • You are saying it will replace one (which I think is what happens). I do not understand how this is consistent with the documentation, which says "This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId...". My reading of that is that all of the MainActivityFragments should be removed, not just one. – mattm Jun 05 '15 at 19:41
4

@Emanuel is correct that the UI state is retained across screen rotations. However what's happening is simply that your Activity's onCreate is always called on rotation, which always adds your startingFragment

The solution is simple. Just add a condition in your onCreate as so to handle the case when it's called as part of the activity lifecycle:

if (savedInstanceState == null)
        {
            FragmentTransaction t = getFragmentManager().beginTransaction();
            t.add(R.id.fragment_container, startingFragment, "start").commit();
        }

You might want to consider doing

t.replace(R.id.fragment_container, startingFragment, "start").commit();

though instead of

t.add(R.id.fragment_container, startingFragment, "start").commit();
Cigogne Eveillée
  • 2,178
  • 22
  • 36
  • Out of curiosity, why do the multiple adds for the same fragment not result in "java.lang.IllegalStateException: Fragment already added"? – mattm Jun 05 '15 at 17:50
  • While savedInstanceState == null works too it's less robust than checking for the existence of the fragments in the ui tree and java.lang.IllegalStateException: Fragment already added isn't thrown because you add a new fragment not the same one. – Emanuel Moecklin Jun 05 '15 at 18:15