1.2: Communicating with a Fragment

Contents:

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:

  • Create a Fragment with an interactive UI.
  • Add a static Fragment to the UI layout of an Activity.
  • Add, replace, and remove a dynamic Fragment while an Activity is running.

What you will LEARN

You will learn how to:

  • Send data to a Fragment.
  • Retrieve data from a Fragment.
  • Implement a master/detail layout for wide screens.

What you will DO

  • Use an argument Bundle to send data to a Fragment.
  • Define a listener interface with a callback method in a Fragment.
  • Implement the listener for the Fragment and a callback to retrieve data from the Fragment.
  • Move Activity code to a Fragment for a master/detail layout.

Apps overview

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.  Tapping Open adds the fragment. Tapping Close removes the fragment.

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:

  • Use a Fragment to show a master/detail layout on tablets.
  • Provide the Fragment with the information it needs to perform tasks.

On a mobile phone screen, the SongDetail app looks like the following figure:  Tap a song title to see the song details on a mobile phone.

On a tablet in horizontal orientation, the SongDetail app displays a master/detail layout:  Song details appear next to the song list in a master/detail layout.

Task 1. Communicating with a fragment

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:

  • Define a listener interface in the Fragment with a callback method to get the user's choice.
  • Implement the interface and callback in the host Activity to retrieve the user's choice.
  • Use the newInstance() factory method to provide the user's choice back to the Fragment when creating the next Fragment instance.

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 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;
    
  3. Add a variable for the radio button choice:
     public int mRadioButtonChoice = NONE;
    
  4. Define a listener interface called OnFragmentInteractionListener. In it, specify a callback method that you will create, called onRadioButtonChoice():
     interface OnFragmentInteractionListener {
         void onRadioButtonChoice(int choice);
     }
    
  5. 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;
    
  6. 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".

  7. 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 {
    
  2. 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.
  3. 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).
    
  4. 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();
     }
    
  5. 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").  Choosing a radio button in the FragmentExample app now displays a toast with the numeric value of the choice (0 is for Yes).

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);
    
  2. Open SimpleFragment and add the following constant, which is the key to finding the information in the Bundle:
     private static final String CHOICE = "choice";
    
  3. 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;
     }
    
  4. 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());
         }
     }
    
  5. Run the app. At first, the app doesn't show the Fragment (see the left side of the following figure).
  6. Tap Open and make a choice such as "Yes" (see the center of the figure below). The choice you made appears in a Toast.
  7. Tap Close to close the Fragment.
  8. 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.  Click Open (left), make a radio button choice (center), click Close to close the fragment, and tap Open to reopen it; the choice is retained (right).

Task 1 solution code

Android Studio project: FragmentCommunicate

Task 2. Changing an app to a master/detail layout

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.  Tapping a song title to see the song details on a mobile phone.

On a tablet, the app doesn't take advantage of the full screen size, as shown in the following figure:  Tapping a song title to see the song details on a tablet.

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.  Song details appear next to the song list in a master/detail layout.

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).  Displaying song detail in the starter app (1) and the final version of the app (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 "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.  Tapping a song title to see the song details on a tablet.

  3. 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:

  • song_list.xml as the default for any screen size.
  • song_list.xml (w900dp) for devices with screens that have a width of 900dp or larger. It differs from song_list.xml because it includes a FrameLayout with an id of song_detail_container for displaying the Fragment on a wide screen.

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:

  • song_detail.xml: Included within activity_song_detail.xml to define the layout of the TextView for the detailed song information.
  • activity_song_list.xml: Layout for MainActivity. This layout includes song_list.xml.
  • song_list_content.xml: Item layout for the RecyclerView adapter.

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;
    
  3. Open SongDetailFragment, and Edit > Paste the above declaration at the top of the class.
  4. 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;
     }
    
  5. 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.

  • If the display is wide enough for a two-pane layout, MainActivity will host the Fragment, and send the position of the selected song in the list directly to the Fragment.
  • If the screen is not wide enough for a two-pane layout, MainActivity will use an intent with extra data—the position of the selected song—to start SongDetailActivity. SongDetailActivity will then host the Fragment, and send the position of the selected song to the Fragment.

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;
    
  2. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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));
         }
     }
    
  7. 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.)
  8. 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.  Song details appear next to the song list in a master/detail layout.

Task 2 solution code

Android Studio project: SongDetail

Summary

Adding a Fragment dynamically:

  • When adding a Fragment dynamically to an Activity, the best practice for representing the Fragment as an instance in the Activity is to create the instance with a newinstance() factory method in the Fragment.
  • The newinstance() method can set a Bundle and use setArguments(Bundle) to supply the construction arguments for the Fragment.
  • Call the newinstance() method from the Activity to create a new instance, and pass the specific data you need for this Bundle.

Fragment lifecycle:

  • The system calls onAttach() when a Fragment is first associated with an Activity. Use onAttach() to initialize essential components of the Fragment, such as a listener.
  • The system calls onCreate() when creating a Fragment. Use onCreate() to initialize components of the Fragment that you want to retain when the Fragment is paused or stopped, then resumed.
  • The system calls onCreateView() to draw a Fragment UI for the first time. To draw a UI for your Fragment, you must return the root View of your Fragment layout from this method. You can return null if the Fragment does not provide a UI.
  • When a Fragment is in the active or resumed state, it can access the host Activity instance with getActivity()and easily perform tasks such as finding a View in the Activity layout.

Calling Fragment methods and saving its state:

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

  • setArguments(Bundle): Supply the construction arguments for a Fragment. The arguments are retained across the Fragment lifecycle.
  • getArguments(): Return the arguments supplied to setArguments(Bundle), if any.

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

  • The interface in the Fragment defines a callback method to communicate to its host Activity.
  • The host Activity implements the callback method.

The related concept documentation is Fragment lifecycle and communications.

Learn more

Android developer documentation:

Videos:

results matching ""

    No results matching ""