This codelab is part of the Advanced Android Development training course, developed by the Google Developers Training team. You will get the most value out of this course if you work through the codelabs in sequence.

For complete details about the course, see the Advanced Android Development overview.

Introduction

An Activity hosting a Fragment can send data to and receive data from the Fragment. A Fragment can't communicate directly with another Fragment, even within the same Activity. The host Activity must be used as an intermediary.

This practical demonstrates how to use an Activity to communicate with a Fragment. It also shows how to use a Fragment to implement a two-pane master/detail layout. A master/detail layout is a layout that allows users to view a list of items (the master view) and drill down into each item for more details (the detail view).

What you should already know

You should be able to:

What you'll learn

What you'll do

In FragmentExample2, the app from the lesson on using a Fragment, a user can tap the Open button to show the Fragment, tap a radio button for a "Yes" or "No" choice, and tap Close to close the Fragment. If the user opens the Fragment again, the previous choice is not retained.

In this lesson you modify the code in the FragmentExample2 app to send the user's choice back to the host Activity. When the Activity opens a new Fragment, the Activity can send the user's previous choice to the new Fragment. As a result, when the user taps Open again, the app shows the previously selected choice.

The second app, SongDetail, demonstrates how you can:

On a mobile phone screen, the SongDetail app looks like the following figure:

On a tablet in horizontal orientation, the SongDetail app displays a master/detail layout:

You will modify the FragmentExample2 app from the lesson on creating a Fragment with a UI, so that the Fragment can tell the host Activity which choice ("Yes" or "No") the user made. You will:

1.1 Define a listener interface with a callback

To define a listener interface in the Fragment:

  1. Copy the FragmentExample2 project in order to preserve it as an intermediate step. Open the new copy in Android Studio, and refactor and rename the new project to FragmentCommunicate. (For help with copying projects and refactoring and renaming, see How to copy and rename a project.)
  2. Open SimpleFragment, and add another constant in the SimpleFragment class to represent the third state of the radio button choice, which is 2 if the user has not yet made a choice:
private static final int NONE = 2;
  1. Add a variable for the radio button choice:
public int mRadioButtonChoice = NONE;
  1. Define a listener interface called OnFragmentInteractionListener. In it, specify a callback method that you will create, called onRadioButtonChoice():
interface OnFragmentInteractionListener {
    void onRadioButtonChoice(int choice);
}
  1. Define a variable for the listener at the top of the SimpleFragment class. You will use this variable in onAttach() in the next step:
OnFragmentInteractionListener mListener;
  1. Override the onAttach() lifecycle method in SimpleFragment to capture the host Activity interface implementation:
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    if (context instanceof OnFragmentInteractionListener) {
        mListener = (OnFragmentInteractionListener) context;
    } else {
        throw new ClassCastException(context.toString()
           + getResources().getString(R.string.exception_message));
    }
}

The onAttach() method is called as soon as the Fragment is associated with the Activity. The code makes sure that the host Activity has implemented the callback interface. If not, it throws an exception.

The string resource exception_message is included in the starter app for the text "must implement OnFragmentInteractionListener".

  1. In onCreateView(), get the mRadioButtonChoice by adding code to each case of the switch case block for the radio buttons:
case YES: // User chose "Yes."
    textView.setText(R.string.yes_message);
    mRadioButtonChoice = YES;
    mListener.onRadioButtonChoice(YES);
    break;
case NO: // User chose "No."
    textView.setText(R.string.no_message);
    mRadioButtonChoice = NO;
    mListener.onRadioButtonChoice(NO);
    break;
default: // No choice made.
    mRadioButtonChoice = NONE;
    mListener.onRadioButtonChoice(NONE);
    break;

1.2 Implement the callback in the Activity

To implement the callback:

  1. Open MainActivity and implement OnFragmentInteractionListener in order to receive the data from the Fragment:
public class MainActivity extends AppCompatActivity
        implements SimpleFragment.OnFragmentInteractionListener {
  1. In Android Studio, the above code is underlined in red, and a red bulb appears in the left margin. Click the bulb and choose Implement methods. Choose onRadioButtonChoice(choice:int):void, and click OK. An empty onRadioButtonChoice() method appears in MainActivity.
  2. Add a member variable in MainActivity for the choice the user makes with the radio buttons, and set it to the default value:
private int mRadioButtonChoice = 2; // The default (no choice).
  1. Add code to the new onRadioButtonChoice() method to assign the user's radio button choice from the Fragment to mRadioButtonChoice. Add a Toast to show that the Activity has received the data from the Fragment:
@Override
public void onRadioButtonChoice(int choice) {
    // Keep the radio button choice to pass it back to the fragment.
    mRadioButtonChoice = choice;
    Toast.makeText(this, "Choice is " + Integer.toString(choice), 
                                       Toast.LENGTH_SHORT).show();
}
  1. Run the app, tap Open, and make a choice. The Activity shows the Toast message with the choice as an integer (0 is "Yes" and 1 is "No").

1.3 Provide the user's choice to the fragment

To provide the user's previous "Yes" or "No" choice from the host Activity to the Fragment, pass the choice to the newInstance() method in SimpleFragment when instantiating SimpleFragment. You can set a Bundle and use the Fragment.setArguments(Bundle) method to supply the construction arguments for the Fragment.

Follow these steps:

  1. Open MainActivity, and change the newInstance() statement in displayFragment() to the following:
SimpleFragment fragment = SimpleFragment.newInstance(mRadioButtonChoice);
  1. Open SimpleFragment and add the following constant, which is the key to finding the information in the Bundle:
private static final String CHOICE = "choice";
  1. In SimpleFragment, change the newInstance() method to the following, which uses a Bundle and the setArguments(Bundle) method to set the arguments before returning the Fragment:
public static SimpleFragment newInstance(int choice) {
        SimpleFragment fragment = new SimpleFragment();
        Bundle arguments = new Bundle();
        arguments.putInt(CHOICE, choice);
        fragment.setArguments(arguments);
        return fragment;
}
  1. Now that a Bundle of arguments is available in the SimpleFragment, you can add code to the onCreateView() method in SimpleFragment to get the choice from the Bundle. Right before the statement that sets the radioGroup onCheckedChanged listener, add the following code to retrieve the radio button choice (if a choice was made), and pre-select the radio button.
if (getArguments().containsKey(CHOICE)) {
    // A choice was made, so get the choice.
    mRadioButtonChoice = getArguments().getInt(CHOICE);
    // Check the radio button choice.
    if (mRadioButtonChoice != NONE) {
        radioGroup.check
                 (radioGroup.getChildAt(mRadioButtonChoice).getId());
    }
}
  1. Run the app. At first, the app doesn't show the Fragment (see the left side of the following figure).
  2. Tap Open and make a choice such as "Yes" (see the center of the figure below). The choice you made appears in a Toast.
  3. Tap Close to close the Fragment.
  4. Tap Open to reopen the Fragment. The Fragment appears with the choice already made (see the right side of the figure). Your app has communicated the choice from the Fragment to the Activity, and then back to the Fragment.

Task 1 solution code

Android Studio project: FragmentCommunicate

This task demonstrates how you can use a Fragment to implement a two-pane master/detail layout for a horizontal tablet display. It also shows how to take code from an Activity and encapsulate it within a Fragment, thereby simplifying the Activity.

In this task you use a starter app called SongDetail_start that displays song titles that the user can tap to see song details.

On a tablet, the app doesn't take advantage of the full screen size, as shown in the following figure:

When set to a horizontal orientation, a tablet device is wide enough to show information in a master/detail layout. You will modify the app to show a master/detail layout if the device is wide enough, with the song list as the master, and the Fragment as the detail, as shown in the following figure.

The following diagram shows the difference in the code for the SongDetail starter app (1), and the final version of the app for both mobile phone and wide tablets (2-3).

In the above figure:

  1. Phone or tablet: The SongDetail_start app displays the song details in a vertical layout in SongDetailActivity, which is called from MainActivity.
  2. Phone or small screen: The final version of SongDetail displays the song details in SongDetailFragment. MainActivity calls SongDetailActivity, which then hosts the Fragment in a vertical layout.
  3. Tablet or larger screen in horizontal orientation: If the screen is wide enough for the master/detail layout, the final version of SongDetail displays the song details in SongDetailFragment. MainActivity hosts the Fragment directly.

2.1 Examine the starter app layout

To save time, download the SongDetail_start starter app, which has been prepared with data, layouts, and a RecyclerView.

  1. Open the SongDetail_start app in Android Studio, and rename and refactor the project to SongDetail (for help with copying projects and refactoring and renaming, see How to copy and rename a project).
  2. Run the app on a tablet or a tablet emulator in horizontal orientation. For instructions on using the emulator, see Run Apps on the Android Emulator. The starter app uses the same layout for tablets and mobile phones—it doesn't take advantage of a wide screen.

  1. Examine the layouts. Although you don't need to change them, you will reference the android:id values in your code.

The song_list.xml layout is included within activity_song_list.xml to define the layout of the song list. You can expand it to show:

The activity_song_detail.xml layout for SongDetailActivity includes song_detail.xml. Provided is a FrameLayout with the same id of song_detail_container for displaying the Fragment on a screen that is not wide.

The following layouts are also provided, which you don't have to change:

2.2 Examine the starter app code

Open SongDetailActivity and find the code in the onCreate() method that displays the song detail:

// ...
// This activity displays the detail. In a real-world scenario,
// get the data from a content repository.
mSong = SongUtils.SONG_ITEMS.get
                (getIntent().getIntExtra(SongUtils.SONG_ID_KEY, 0));

// Show the detail information in a TextView.
if (mSong != null) {
    ((TextView) findViewById(R.id.song_detail))
                    .setText(mSong.details);
}
// ...

In the next step you will add a new Fragment, and copy the if (mSong != null) block with setText() to the new Fragment, so that the Fragment controls how the song detail is displayed.

The SongUtils.java class in the content folder creates an array of fixed entries for the song title and song detail information. You can modify this class to refer to different types of data. However, in a real-world production app, you would most likely get data from a repository or server, rather than hardcoding it in the app.

2.3 Add the fragment

Add a new blank Fragment, and move code from SongDetailActivity to the Fragment, so that the Fragment can take over the job of displaying the song detail.

  1. Select the app package name within java in the Project: Android view, add a new Fragment (Blank), and name the Fragment SongDetailFragment. Uncheck the Include fragment factory methods and Include interface callbacks options.
  2. Open SongDetailActivity, and Edit > Cut the mSong variable declaration from the Activity. Some of the code in SongDetailActivity that relies on it will be underlined in red, but you will replace that code in subsequent steps.
public SongUtils.Song mSong;
  1. Open SongDetailFragment, and Edit > Paste the above declaration at the top of the class.
  2. In SongDetailFragment, remove all code in the onCreateView() method and change it to inflate the song_detail.xml layout:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
    View rootView = 
            inflater.inflate(R.layout.song_detail, container, false);
    // TODO: Show the detail information in a TextView.
    return rootView;
}
  1. To use the song detail in the Fragment, replace the TODO comment with the if (mSong != null) block from SongDetailActivity, which includes the setText() method to show the detail information in the song_detail TextView. You need to add rootView to use the findViewById() method; otherwise, the block is the same as the one formerly used in SongDetailActivity:
if (mSong != null) {
            ((TextView) rootView.findViewById(R.id.song_detail))
                    .setText(mSong.details);
}

2.4 Check if the screen is wide enough for a two-pane layout

MainActivity in the starter app provides the data to a second Activity (SongDetailActivity) to display the song detail on a separate Activity display. To change the app to provide data for the Fragment, you will change the code that displays the song detail.

In other words, the Fragment will take over the job of displaying the song detail. Therefore, your code needs to host the Fragment in MainActivity if the screen is wide enough for a two-pane display, or in SongDetailActivity if the screen is not wide enough.

Open MainActivity, and follow these steps:

  1. To serve as a check for the size of the screen, add a private boolean to the MainActivity class called mTwoPane:
private boolean mTwoPane = false;
  1. Add the following to the end of the MainActivity onCreate() method:
if (findViewById(R.id.song_detail_container) != null) {
    mTwoPane = true;
}

The above code checks for the screen size and orientation. The song_detail_container view for MainActivity will be present only if the screen's width is 900dp or larger, because it is defined only in the song_list.xml (w900dp) layout, not in the default song_list.xml layout for smaller screen sizes. If this view is present, then the Activity should be in two-pane mode.

If a tablet is set to portrait orientation, its width will most likely be lower than 900dp, and so it will not show a two-pane layout. If the tablet is set to horizontal orientation and its width is 900dp or larger, it will show a two-pane layout.

2.5 Use the fragment to show song detail

The Fragment needs to know which song title the user selected. To use the same best practice for creating an instance of a Fragment, as in the previous exercises, create a newInstance() factory method in the Fragment.

In the newInstance() method you can set a Bundle and use the Fragment.setArguments(Bundle) method to supply the construction arguments for the Fragment. In a following step, you will use the Fragment.getArguments() method in the Fragment to get the arguments supplied by setArguments(Bundle).

  1. Open SongDetailFragment, and add the following method to it:
public static SongDetailFragment newInstance (int selectedSong) {
    SongDetailFragment fragment = new SongDetailFragment();
    // Set the bundle arguments for the fragment.
    Bundle arguments = new Bundle();
    arguments.putInt(SongUtils.SONG_ID_KEY, selectedSong);
    fragment.setArguments(arguments);
    return fragment;
}

The above method receives the selectedSong (the integer position of the song title in the list), and creates the arguments Bundle with SONG_ID_KEY and selectedSong. It then uses setArguments(arguments) to set the arguments for the Fragment, and returns the Fragment.

  1. Open MainActivity, and find the onBindViewHolder() method that implements a listener with setOnClickListener(). When the user taps a song title, the starter app code starts SongDetailActivity using an intent with extra data (the position of the selected song in the list):
holder.mView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Context context = v.getContext();
        Intent intent = new Intent(context, SongDetailActivity.class);
        intent.putExtra(SongUtils.SONG_ID_KEY,
                                holder.getAdapterPosition());
        context.startActivity(intent);
    }
});

This code sends the extra data—SongUtils.SONG_ID_KEY and holder.getAdapterPosition())—that SongDetailActivity uses to show the correct detail for the tapped song title. You will have to send this data to the new Fragment.

  1. Change the code within the onClick() method to create a new instance of the Fragment for two-pane display, or to use the intent (as before) to launch the second Activity if not a two-pane display.
if (mTwoPane) {
    int selectedSong = holder.getAdapterPosition();
    SongDetailFragment fragment =
                          SongDetailFragment.newInstance(selectedSong);
    getSupportFragmentManager().beginTransaction()
                         .replace(R.id.song_detail_container, fragment)
                         .addToBackStack(null)
                         .commit();
} else {
    Context context = v.getContext();
    Intent intent = new Intent(context, SongDetailActivity.class);
    intent.putExtra(SongUtils.SONG_ID_KEY,
                                holder.getAdapterPosition());
    context.startActivity(intent);
}

If mTwoPane is true, the code gets the selected song position (selectedSong) in the song title list, and passes it to the new instance of SongDetailFragment using the newInstance() method in the Fragment. It then uses getSupportFragmentManager() with a replace transaction to show a new version of the Fragment.

The transaction code for managing a Fragment should be familiar, as you performed such operations in a previous lesson. By replacing the Fragment, you can refresh with new data a Fragment that is already running.

If mTwoPane is false, the code does exactly the same thing it did in the starter app: it starts SongDetailActivity with an intent and SONG_ID_KEY and holder.getAdapterPosition() as extra data.

  1. Open SongDetailActivity, and find the code in the onCreate() method that no longer works due to the removal of the mSong declaration. In the next step you will replace it.
// This activity displays the detail. In a real-world scenario,
// get the data from a content repository.
mSong = SongUtils.SONG_ITEMS.get
                (getIntent().getIntExtra(SongUtils.SONG_ID_KEY, 0));
// Show the detail information in a TextView.
if (mSong != null) {
    ((TextView) findViewById(R.id.song_detail))
                    .setText(mSong.details);
}

Previously you cut the if (mSong != null) block that followed the above code and pasted it into the Fragment, so that the Fragment could display the song detail. You can now replace the above code in the next step so that SongDetailActivity will use the Fragment to display the song detail.

  1. Replace the above code in onCreate() with the following code. It first checks if savedInstanceState is null, which means the Activity started but its state was not saved. If it is null, it creates an instance of the Fragment, passing it the selectedSong. (If savedInstanceState is not null, the Activity state has been saved—such as when the screen is rotated. In such cases, you don't need to add the Fragment.)
if (savedInstanceState == null) {
    int selectedSong =
                    getIntent().getIntExtra(SongUtils.SONG_ID_KEY, 0);
    SongDetailFragment fragment =
                    SongDetailFragment.newInstance(selectedSong);
    getSupportFragmentManager().beginTransaction()
                    .add(R.id.song_detail_container, fragment)
                    .commit();
}

The code first gets the selected song title position from the intent extra data. It then creates an instance of the Fragment and adds it to the Activity using a Fragment transaction. SongDetailActivity will now use the SongDetailFragment to display the detail.

  1. To set up the data in the Fragment, open SongDetailFragment and add the entire onCreate() method before the onCreateView() method. The getArguments() method in the onCreate() method gets the arguments supplied to the Fragment using setArguments(Bundle).
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments().containsKey(SongUtils.SONG_ID_KEY)) {
        // Load the content specified by the fragment arguments.
        mSong = SongUtils.SONG_ITEMS.get(getArguments()
                    .getInt(SongUtils.SONG_ID_KEY));
    }
}
  1. Run the app on a mobile phone or phone emulator. It should look the same as it did before. (Refer to the figure at the beginning of this task.)
  2. Run the app on a tablet or tablet emulator in horizontal orientation. It should display the master/detail layout as shown in the following figure.

Task 2 solution code

Android Studio project: SongDetail

Adding a Fragment dynamically:

Fragment lifecycle:

Calling Fragment methods and saving its state:

To communicate from the host Activity to a Fragment, use a Bundle and the following:

To have a Fragment communicate to its host Activity, declare an interface in the Fragment, and implement it in the Activity.

The related concept documentation is 1.2 Fragment lifecycle and communications.

Android developer documentation:

Videos:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

In the FragmentCommunicate app, when the user makes a choice in the fragment, the app shows the user's choice in a Toast message. The Toast shows "Choice is 0" for "Yes" or "Choice is 1" for "No."

Change the FragmentCommunicate app so that the Toast shows the "Yes" or "No" message rather than "0" or "1."

Answer these questions

Question 1

Which fragment-lifecycle callback draws the fragment's UI for the first time?

Question 2

How do you send data from an activity to a fragment?

Submit your app for grading

Guidance for graders

Check that the app has the following feature:

To see all the codelabs in the Advanced Android Development training course, visit the Advanced Android Development codelabs landing page.