1.2: Communicating with a Fragment
Contents:
- What you should already KNOW
- What you will LEARN
- What you will DO
- Apps overview
- Task 1. Communicating with a fragment
- Task 1 solution code
- Task 2. Changing an app to a master/detail layout
- Task 2 solution code
- Summary
- Related concept
- Learn more
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 anActivity
. - Add, replace, and remove a dynamic
Fragment
while anActivity
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 aFragment
. - Define a listener interface with a callback method in a
Fragment
. - Implement the listener for the
Fragment
and a callback to retrieve data from theFragment
. - Move
Activity
code to aFragment
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.
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:
On a tablet in horizontal orientation, the SongDetail app displays 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 theFragment
when creating the nextFragment
instance.
1.1 Define a listener interface with a callback
To define a listener interface in the Fragment
:
- 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 toFragmentCommunicate
. (For help with copying projects and refactoring and renaming, see Copy and rename a project.) - Open
SimpleFragment
, and add another constant in theSimpleFragment
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;
- Add a variable for the radio button choice:
public int mRadioButtonChoice = NONE;
- Define a listener interface called
OnFragmentInteractionListener
. In it, specify a callback method that you will create, calledonRadioButtonChoice()
:interface OnFragmentInteractionListener { void onRadioButtonChoice(int choice); }
- Define a variable for the listener at the top of the
SimpleFragment
class. You will use this variable inonAttach()
in the next step:OnFragmentInteractionListener mListener;
Override the
onAttach()
lifecycle method inSimpleFragment
to capture the hostActivity
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 theFragment
is associated with theActivity
. The code makes sure that the hostActivity
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".In
onCreateView()
, get themRadioButtonChoice
by adding code to eachcase
of theswitch 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:
- Open
MainActivity
and implementOnFragmentInteractionListener
in order to receive the data from theFragment
:public class MainActivity extends AppCompatActivity implements SimpleFragment.OnFragmentInteractionListener {
- 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 inMainActivity
. - 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).
- Add code to the new
onRadioButtonChoice()
method to assign the user's radio button choice from theFragment
tomRadioButtonChoice
. Add aToast
to show that theActivity
has received the data from theFragment
:@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(); }
- Run the app, tap Open, and make a choice. The
Activity
shows theToast
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:
- Open
MainActivity
, and change thenewInstance()
statement indisplayFragment()
to the following:SimpleFragment fragment = SimpleFragment.newInstance(mRadioButtonChoice);
- Open
SimpleFragment
and add the following constant, which is the key to finding the information in theBundle
:private static final String CHOICE = "choice";
- In
SimpleFragment
, change thenewInstance()
method to the following, which uses aBundle
and thesetArguments(Bundle)
method to set the arguments before returning theFragment
:public static SimpleFragment newInstance(int choice) { SimpleFragment fragment = new SimpleFragment(); Bundle arguments = new Bundle(); arguments.putInt(CHOICE, choice); fragment.setArguments(arguments); return fragment; }
- Now that a
Bundle
of arguments is available in theSimpleFragment
, you can add code to theonCreateView()
method inSimpleFragment
to get the choice from theBundle
. Right before the statement that sets theradioGroup
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()); } }
- Run the app. At first, the app doesn't show the
Fragment
(see the left side of the following figure). - Tap Open and make a choice such as "Yes" (see the center of the figure below). The choice you made appears in a
Toast
. - Tap Close to close the
Fragment
. - Tap Open to reopen the
Fragment
. TheFragment
appears with the choice already made (see the right side of the figure). Your app has communicated the choice from theFragment
to theActivity
, and then back to theFragment
.
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.
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:
- Phone or tablet: The SongDetail_start app displays the song details in a vertical layout in
SongDetailActivity
, which is called fromMainActivity
. - Phone or small screen: The final version of SongDetail displays the song details in
SongDetailFragment
.MainActivity
callsSongDetailActivity
, which then hosts theFragment
in a vertical layout. - 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 theFragment
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
.
- 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"). 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.
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 fromsong_list.xml
because it includes aFrameLayout
with an id ofsong_detail_container
for displaying theFragment
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 withinactivity_song_detail.xml
to define the layout of theTextView
for the detailed song information.activity_song_list.xml
: Layout forMainActivity
. This layout includessong_list.xml
.song_list_content.xml
: Item layout for theRecyclerView
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.
- Select the app package name within
java
in the Project: Android view, add a new Fragment (Blank), and name theFragment
SongDetailFragment. Uncheck the Include fragment factory methods and Include interface callbacks options. - Open
SongDetailActivity
, and Edit > Cut themSong
variable declaration from theActivity
. Some of the code inSongDetailActivity
that relies on it will be underlined in red, but you will replace that code in subsequent steps.public SongUtils.Song mSong;
- Open
SongDetailFragment
, and Edit > Paste the above declaration at the top of the class. - In
SongDetailFragment
, remove all code in theonCreateView()
method and change it to inflate thesong_detail.xm
l 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; }
- To use the song detail in the
Fragment
, replace theTODO
comment with theif (mSong != null)
block fromSongDetailActivity
, which includes thesetText()
method to show the detail information in thesong_detail
TextView
. You need to addrootView
to use thefindViewById()
method; otherwise, the block is the same as the one formerly used inSongDetailActivity
: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 theFragment
, and send the position of the selected song in the list directly to theFragment
. - 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 startSongDetailActivity
.SongDetailActivity
will then host theFragment
, and send the position of the selected song to theFragment
.
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:
- To serve as a check for the size of the screen, add a
private
boolean
to theMainActivity
class calledmTwoPane
:private boolean mTwoPane = false;
- Add the following to the end of the
MainActivity
onCreate()
method:
The above code checks for the screen size and orientation. Theif (findViewById(R.id.song_detail_container) != null) { mTwoPane = true; }
song_detail_container
view forMainActivity
will be present only if the screen's width is 900dp or larger, because it is defined only in thesong_list.xml (w900dp)
layout, not in the defaultsong_list.xml
layout for smaller screen sizes. If this view is present, then theActivity
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)
.
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 thearguments
Bundle
withSONG_ID_KEY
andselectedSong
. It then usessetArguments(arguments)
to set the arguments for theFragment
, and returns theFragment
.Open
MainActivity
, and find theonBindViewHolder()
method that implements a listener withsetOnClickListener()
. When the user taps a song title, the starter app code startsSongDetailActivity
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
andholder.getAdapterPosition()
)—thatSongDetailActivity
uses to show the correct detail for the tapped song title. You will have to send this data to the newFragment
.Change the code within the
onClick()
method to create a new instance of theFragment
for two-pane display, or to use the intent (as before) to launch the secondActivity
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 ofSongDetailFragment
using thenewInstance()
method in theFragment
. It then usesgetSupportFragmentManager()
with areplace
transaction to show a new version of theFragment
.The transaction code for managing a
Fragment
should be familiar, as you performed such operations in a previous lesson. By replacing theFragment
, you can refresh with new data aFragment
that is already running.If
mTwoPane
is false, the code does exactly the same thing it did in the starter app: it startsSongDetailActivity
with an intent andSONG_ID_KEY
andholder.getAdapterPosition()
as extra data.Open
SongDetailActivity
, and find the code in theonCreate()
method that no longer works due to the removal of themSong
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 theFragment
, so that theFragment
could display the song detail. You can now replace the above code in the next step so thatSongDetailActivity
will use theFragment
to display the song detail.Replace the above code in
onCreate()
with the following code. It first checks ifsavedInstanceState
isnull
, which means theActivity
started but its state was not saved. If it isnull
, it creates an instance of theFragment
, passing it theselectedSong
. (IfsavedInstanceState
is notnull
, theActivity
state has been saved—such as when the screen is rotated. In such cases, you don't need to add theFragment
.)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 theActivity
using aFragment
transaction.SongDetailActivity
will now use theSongDetailFragment
to display the detail.To set up the data in the
Fragment
, openSongDetailFragment
and add the entireonCreate()
method before theonCreateView()
method. ThegetArguments()
method in theonCreate()
method gets the arguments supplied to theFragment
usingsetArguments(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)); } }
- 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.)
- 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
Summary
Adding a Fragment
dynamically:
- When adding a
Fragment
dynamically to anActivity
, the best practice for representing theFragment
as an instance in theActivity
is to create the instance with anewinstance()
factory method in theFragment
. - The
newinstance()
method can set aBundle
and usesetArguments(Bundle)
to supply the construction arguments for theFragment
. - Call the
newinstance()
method from theActivity
to create a new instance, and pass the specific data you need for thisBundle
.
Fragment lifecycle:
- The system calls
onAttach()
when aFragment
is first associated with anActivity
. UseonAttach()
to initialize essential components of theFragment
, such as a listener. - The system calls
onCreate()
when creating aFragment
. UseonCreate()
to initialize components of theFragment
that you want to retain when theFragment
is paused or stopped, then resumed. - The system calls
onCreateView()
to draw aFragment
UI for the first time. To draw a UI for yourFragment
, you must return the rootView
of yourFragment
layout from this method. You can returnnull
if theFragment
does not provide a UI. - When a
Fragment
is in the active or resumed state, it can access the hostActivity
instance withgetActivity()
and easily perform tasks such as finding aView
in theActivity
layout.
Calling Fragment
methods and saving its state:
- The host
Activity
can call methods in aFragment
by acquiring a reference to theFragment
fromFragmentManager
, usingfindFragmentById()
. - Save the
Fragment
state during the onSaveInstanceState() callback and restore it during eitheronCreate()
,onCreateView()
, oronActivityCreated()
.
To communicate from the host Activity
to a Fragment
, use a Bundle
and the following:
setArguments(Bundle)
: Supply the construction arguments for aFragment
. The arguments are retained across theFragment
lifecycle.getArguments()
: Return the arguments supplied tosetArguments(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 hostActivity
. - The host
Activity
implements the callback method.
Related concept
The related concept documentation is Fragment lifecycle and communications.
Learn more
Android developer documentation:
- Fragment
- Fragments
- FragmentManager
- FragmentTransaction
- Creating a Fragment
- Communicating with Other Fragments
- The Activity Lifecycle
- Building a Flexible UI
- Building a Dynamic UI with Fragments
- Handling Configuration Changes
- Tasks and Back Stack
Videos:
- What the Fragment? (Google I/O 2016)
- Fragment Tricks (Google I/O '17)
- Por que Precisamos de Fragments?