Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Switching Fragments in Master/Detail Flow

I am attempting to create an app which has a Master/Detail flow using Fragments. Selecting an item will open a detail fragment which may then which to "open" another fragment and add it to the back stack.

I have renamed classes to help illustrate what they do.

public class ListOfDetails extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    //Callback method indicating that an item with the given ID was selected.
    public void onItemSelected(String id) {
        // Performing logic to determine what fragment to start omitted

        if (ifTwoPanes()) {
            Fragment fragment = new DetailFragmentType1();
            getSupportFragmentManager().beginTransaction().replace(R.id.aContainer, fragment).commit();
        } else {
            Intent newIntent = new Intent(this, SinglePaneFragmentWrapper.class);
            newIntent.putExtra("id", id);
            startActivity(newIntent);
        }
    }

    // My attempt at making it possible to change displayed fragment from within fragments
    public void changeDetailFragment(Fragment fragment) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        transaction.addToBackStack(null);
        transaction.replace(R.id.aContainer, fragment);
        transaction.commit();
    }
}

An example of one of the detail fragments. There are many different Fragments that may be created in different circumstances.

public class DetailFragmentType1 extends Fragment {
    private ListOfDetails parent;

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

        Activity a = getActivity();
        if (a instanceof ListOfDetails) {
            parent = (ListOfDetails) a;
        }
    }

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

        Button aButton = (Button) getActivity().findViewById(R.id.aButton);
        aButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                parent.changeDetailFragment(new SubDetailFragment());
            }
        });
    }
}

When on phone, a wrapper activity is used to hold the fragment

public class SinglePaneFragmentWrapper extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Duplicate logic must be performed to start fragment
        // Performing logic to determine what fragment to start omitted
        String id = getIntent().getStringExtra("id");
        if(id == "DetailFragmentType1") {
            Fragment fragment = new DetailFragmentType1();
            getSupportFragmentManager().beginTransaction().replace(R.id.aContainer, fragment).commit();
        } else {
            ...
        }
    }
}

What is the proper way to change the fragment that is open in the detail pane in this circumstance? My method feels like a hack when using two panes and doesn't even work when using only one pane because getParent() from SinglePaneFragmentWrapper returns null, making me unable to call parent.changeDetailFragment().

This is a complicated question, hopefully I explained it well. Let me know if I missed something. Thanks

like image 895
tgrosinger Avatar asked Nov 12 '22 07:11

tgrosinger


1 Answers

There are lots of opinions around this and lots of ways of doing it. I think in this case the problem is "who is responsible for changing the fragment?" on the surface it seems that a listener on the button is the obvious place, but then the fragment shouldn't know what it is hosted in (a symptom of that is getting an undesirable result like null from getParent()).

In your case I would suggest you implement a "listener" interface in the parent and "notify" from the fragment.. when the parent is notified, it changes the fragment. This way the fragment is not changing itself (so doesn't need to know how).. so.. for your case..

Add a new interface:

public interface FragmentChangeListener {
  void onFragmentChangeRequested(Fragment newFragment);
}

Implement the interface in your ListOfDetails activity

public class ListOfDetails extends FragmentActivity implements FragmentChangeListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
}

//Callback method indicating that an item with the given ID was selected.
public void onItemSelected(String id) {
    // Performing logic to determine what fragment to start omitted

    if (ifTwoPanes()) {
        Fragment fragment = new DetailFragmentType1();
        getSupportFragmentManager().beginTransaction().replace(R.id.aContainer, fragment).commit();
    } else {
        Intent newIntent = new Intent(this, SinglePaneFragmentWrapper.class);
        newIntent.putExtra("id", id);
        startActivity(newIntent);
    }
}

// My attempt at making it possible to change displayed fragment from within fragments
public void changeDetailFragment(Fragment fragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
    transaction.addToBackStack(null);
    transaction.replace(R.id.aContainer, fragment);
    transaction.commit();
}

// This is the interface implementation that will be called by your fragments
void onFragmentChangeRequested(Fragment newFragment) {
    changeDetailFragment(newFragment);
}

}

Added listener to detail fragment

public class DetailFragmentType1 extends Fragment {

    private FragmentChangeListener fragmentChangeListener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Actually you might not have an activity here.. you should probably be 
        // doing this in onAttach
        //Activity a = getActivity();
        //if (a instanceof ListOfDetails) {
        //    parent = (ListOfDetails) a;
        //}
    }

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

        Button aButton = (Button) getActivity().findViewById(R.id.aButton);
        aButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
               // parent.changeDetailFragment(new SubDetailFragment());
               notifyFragmentChange(new SubDetailFragment());
            }
        });
    }

    @Override
    public void onAttach(Activity activity) {
      // This is called when the fragment is attached to an activity..
      if (activity instanceof FragmentChangeListener) {
          fragmentChangeListener = (FragmentChangeListener) activity;
      } else {
         // Find your bugs early by making them clear when you can...
         if (BuildConfig.DEBUG) {
           throw new IllegalArgumentException("Fragment hosts must implement FragmentChangeListener");
         }
      }
    }

    private void notifyFragmentChange(Fragment newFragment) {
      FragmentChangeListener listener = fragmentChangeListener;
      if (listener != null) {
         listener.onFragmentChangeRequested(newFragment);
      }
    }
}

And implement the same interface to your single pane activity...

public class SinglePaneFragmentWrapper extends FragmentActivity implements FragmentChangeListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Duplicate logic must be performed to start fragment
        // Performing logic to determine what fragment to start omitted
        String id = getIntent().getStringExtra("id");
        if(id == "DetailFragmentType1") {
            Fragment fragment = new DetailFragmentType1();
            getSupportFragmentManager().beginTransaction().replace(R.id.aContainer, fragment).commit();
        } else {
            ...
        }
    }
// My attempt at making it possible to change displayed fragment from within fragments
public void changeDetailFragment(Fragment fragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
    transaction.addToBackStack(null);
    transaction.replace(R.id.aContainer, fragment);
    transaction.commit();
}

// This is the interface implementation that will be called by your fragments
void onFragmentChangeRequested(Fragment newFragment) {
    changeDetailFragment(newFragment);
}

}

Note the similarity between your single pane and your multi-pane activities.. this suggests that you could either put all of the duplicated code (changefragment etc) into a single activity that they both extend or that in maybe they are the same activities with different layouts...

I hope that helps, Good luck.

Regards, CJ

like image 160
C B J Avatar answered Nov 14 '22 22:11

C B J