CHAPTER NINE: Fragments

Chapter opener image: © Fon_nongkran/Shutterstock

Introduction

In order to support larger screen devices like tablets, Google introduced fragments with API level 11. A fragment is a portion of an activity and can help manage a portion of the screen. We can think of a fragment as a mini-activity within an activity. FIGURE 9.1 shows a screen for Version 4 of this chapter’s app divided into three parts whose backgrounds are blue, red, and green. We can organize that screen so that each part is a fragment. Fragments are reusable. They can be reused within the same app or in a different app. Thus it is desirable to use them.

In this chapter, we demonstrate how to use and manage fragments. We build an app to play the hangman game. We use the Model-View-Controller architecture but we keep the Model as simple as possible so that we can focus on how fragments work. Furthermore, we also discuss how to nest layouts so that each fragment is managed by a separate layout manager.

9.1 The Model

The Model is the Hangman class, shown in EXAMPLE 9.1. It encapsulates a game where the user has to guess all the letters in a word but has only so many tries to do it. We keep our Model to the very minimum, and limit the number of possible words to just a few.

We provide a default value for the number of allowed guesses at line 6 and an array of hard coded words at line 7. At line 8, the instance variable word stores the word to guess. The guessesAllowed and guessesLeft instance variables keep track of the number of guesses allowed and the number of guesses that the user still has. Finally, the indexesGuessed array keeps track of the indexes of the letters in the String word that the user has guessed correctly so far. A value of true means a correct guess at that index.

FIGURE 9.1 The Hangman app, Version 4, running in the horizontal position

EXAMPLE 9.1 The Hangman class

The Hangman constructor, coded at lines 13–23, sets the value of the number of guesses allowed (lines 14–17). It then randomly selects a word within the array words (lines 19–21), and instantiates the indexesGuessed array. We could envision a much more complex Model where another constructor could pull a list of possible words from a file, a database, or even a remote website, and then selects one at random.

We provide methods to play the game (lines 33–43), retrieve the state of completion of the word (lines 45–53), and check if the game is over (lines 55–69). We also provide accessors for guessesAllowed and guessesLeft (lines 25–27 and 29–31).

Figure 9.1 shows an actual game. At the bottom of the left pane, are the number of guesses that the user still has. Inside the upper right pane, are the letters that the user has correctly guessed so far, and we provide an EditText to input the next letter. At the bottom of the right pane, is the status or final result of the game.

9.2 Fragments

Since fragments have been available since API level 11, we need to specify API level 11 or higher when we create the app (at the time of this writing, the default is API level 15).

FIGURE 9.2 Inheritance hierarchy for Fragment and selected subclasses

A fragment must always be embedded inside an activity and although a fragment has its own life-cycle methods, they depend directly on the fragment’s parent activity’s life-cycle methods. For example, if the activity is stopped or destroyed, then the fragment is automatically stopped or destroyed. An activity can have several fragments inside it, and as long as the activity is running, these fragments can have their own life cycles independently of each other. A fragment is reusable and can be used in several activities. A fragment can define its own layout. It may also be invisible and perform tasks in the background.

In order to code our own user-defined fragments, we extend the Fragment class, a direct subclass of Object.

The Fragment class has several subclasses that encapsulate specialized fragments. FIGURE 9.2 shows some of them. A DialogFragment displays a dialog window, which is displayed on top of its activity’s view. A ListFragment displays a list of items and provides event handling when the user selects one of these items. A PreferenceFragment shows a hierarchy of preferences as a list. A WebViewFragment displays a web page.

These subclasses of the Fragment class encapsulate specialized fragment functionalities. For example, the ListFragment class is very useful when we want to display a list of items that the user can interact with in some part of the screen. Clicking on an item may trigger some changes inside another fragment on another part of the screen.

In this chapter, in order to better understand how fragments work, we build our own subclasses of Fragment. We explore the following topics:

  • ▸ How to create and add a fragment for an activity using XML only.

  • ▸ How to create and add a fragment for an activity using XML and code.

  • ▸ How to create and add a fragment for an activity using code only.

  • ▸ How a fragment and its activity communicate.

  • ▸ How to create and add an invisible fragment.

  • ▸ How to make a fragment reusable.

9.3 Defining and Adding a Fragment to an Activity Using a Layout XML File, Hangman, App Version 0

In Version 0 of the app, we show how we define and add a fragment; we do not use the Model yet and we keep the app as simple as possible. We choose the Empty Activity template when we create the app.

EXAMPLE 9.2 shows the activity_main.xml file. There are two elements inside the top LinearLayout element: a fragment element, defined at lines 9–15, and a LinearLayout element, defined at lines 17–37. At line 11, we specify that this fragment is an instance of the GameControlFragment class, which we code later in this section. The top LinearLayout element’s orientation is horizontal(line 6), so the fragment is positioned on the left side of the screen, and the LinearLayout on the right side of the screen. They both have the same android: layout_weight value of 1 (lines 13 and 20), so they both occupy 50% (= 1/(1 + 1)) of the screen, as shown in Figure 9.1 (the fragment is in red, the LinearLayout in blue and green) and illustrated in FIGURE 9.3. The android:layout_weight XML attribute of the LinearLayout. LayoutParams class enables us to specify how much space each View should occupy within its parent LinearLayout. We specify the value of 0dp for the android:layout_width attribute for both the fragment and LinearLayout elements at lines 14 and 21. This is typical when the android_layout:weight attribute is used. The 0dp value means that we want to let the system decide how much space is allocated to the various elements based on the values for the layout_weight attributes.

EXAMPLE 9.2 The activity_main.xml file, Hangman app, Version 0

FIGURE 9.3 Width distribution with weights of 1 and 1

FIGURE 9.4 Width distribution with weights of 1 and 3

If we change the android:layout_weight values to 1 and 3, we get screen proportions of 25% (1/(1 + 3)) and 75% (3/(1 + 3)), as shown in FIGURE 9.4.

Inside the LinearLayout element, we define two LinearLayout elements at lines 23–28 and 30–35. Because that LinearLayout element has its orientation vertical (line 18), the two LinearLayout elements will be at the top and bottom of it (showing in blue and green in Figure 9.1). They have the same android:layout_weight value of 1 (lines 27 and 34), so they both occupy 50% of the right side of the screen (the heights of the blue and green parts are the same on Figure 9.1). To better visualize their position, we color them in blue (line 26) and green (line 33), respectively. Later in this chapter, we will place a fragment in each of them. We must give an id to the fragment element (line 10). If we do not, the app will crash at runtime.

EXAMPLE 9.3 shows the fragment_game_control.xml file (which we place in the layout directory). We use this layout XML file for the left pane of the screen, reserved by the fragment element in Example 9.2. It is managed by a LinearLayout (and its background is red (line 5)).

EXAMPLE 9.3 The fragment_game_control.xml file, Hangman app, Version 0

Like an activity, a fragment has several life-cycle methods, as shown in TABLE 9.1. The methods identified with “a” in the table are automatically called when a fragment is created or restarted. The methods identified with “b” are automatically called when a fragment becomes inactive or is closed. Note that all these methods are public, contrary to the life-cycle methods in the Activity class, which are protected. When we want to inflate a fragment from a layout XML file, we must override the onCreateView method to do so. The onCreateView method provides a LayoutInflater parameter that enables us to inflate an XML file using its inflate method, shown in TABLE 9.2. Its ViewGroup parameter represents the ViewGroup inside which the fragment is to be placed. The Bundle parameter contains data from the previous instance of the fragment. That data can be read when a fragment is resumed.

TABLE 9-1 Life-cycle methods of the Fragment class

Step Method Called
1a void onAttach( Context context ) When the fragment is attached to its context.
2a void onCreate( Bundle savedInstanceState ) When the fragment is created.
3a View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState ) After onCreate; creates and returns the View associated with this fragment.
4a void onViewCreated( View view, Bundle savedInstanceState ) After onCreateView, but before any saved state has been restored to the View.
5a void onActivityCreated( Bundle savedInstanceState ) When the fragment’s activity has finished executing its own onCreate method.
6a void onViewStateRestored( Bundle savedInstanceState ) When the saved state of the fragment’s View hierarchy has been restored (requires API level 17).
7a void onStart( ) When the fragment becomes visible to the user.
8a void onResume( ) When the fragment is visible to the user, who can start interacting with the fragment.
4b void onPause( ) When the activity is being paused or another fragment is modifying it.
3b void onStop( ) When the fragment is no longer visible to the user.
2b void onDestroyView( ) After onStop; the View created by onCreateView has been detached from this fragment.
1b void onDestroy( ) When the fragment is no longer in use.
0b void onDetach( ) Just before the fragment is detached from its activity.

TABLE 9.2 Selected inflate methods of the LayoutInflater class

View inflate( int resource, ViewGroup root, boolean attachToRoot ) Resource is an id for an XML resource that is being inflated. Root is an optional View to be the parent of the inflated layout if attachToRoot is true. If attachToRoot is false, root is only used to set the correct layout parameters for the root view in the XML resource.

EXAMPLE 9.4 shows the GameControlFragment class. At this point, we also keep it to a minimum: it only inflates the fragment_game_control.xml file at lines 16–17, inside the onCreateView method (lines 13–18). The first argument we pass to the inflate method is the resource id for the fragment, fragment_game_control. The second argument is the container that the fragment goes into, the ViewGroup passed by the onCreateView method, container. The third argument is false because in this case, the inflated XML file is already inserted into container. Specifying true would create a redundant ViewGroup hierarchy.

EXAMPLE 9.4 The GameControlFragment class, Hangman app, Version 0

FIGURE 9.5 The Hangman app, Version 0, previewed in landscape orientation

When an activity state is restored, any fragment belonging to the activity is re-instantiated and its default constructor is called automatically. Thus, any fragment class must have a default constructor (lines 10–11).

FIGURE 9.5 shows a preview of the app, Version 0, in landscape orientation.

We can check what methods of the Activity and Fragment classes are automatically called and in what order by adding Log statements inside the various life-cycle methods of both the MainActivity and the GameControlFragment classes. If we add them to both classes, we would see the output shown in FIGURE 9.6 as the user starts the app and interacts with the app and the device. It typically shows two things:

  • ▸ When the activity is created or restarted, a method of the Activity class executes first and a similar method of the Fragment class executes afterward.

  • ▸ When the activity becomes inactive or is closed, a method of the Fragment class executes first and a similar method of the Activity class executes afterward.

9.4 Adding GUI Components, Styles, Strings, and Colors, Hangman, App Version 1

In Version 1 of the app, we populate the left area of the screen (the fragment) with the two GUI components, and we start using the Model.

EXAMPLE 9.5 shows the updated activity_main.xml file. We give ids to the LinearLayout elements (at lines 24 and 32) at the top right and bottom right of the screen so that we can access them later to place fragments in them. We change from hard coded colors to colors defined in the colors.xml file (lines 27 and 35), as shown in EXAMPLE 9.6.

FIGURE 9.6 Output as the user interacts with the HangmanV0LifeCycle app, including life-cycle methods of the Activity and Fragment classes

EXAMPLE 9.5 The activity_main.xml file, Hangman app, Version 1

EXAMPLE 9.6 The colors.xml file, Hangman app, Version 1

EXAMPLE 9.7 shows the updated fragment_game_control.xml file. We place two Linear-Layouts (lines 9–14, 16–20) in it: they each contain a basic GUI component (a button and a label). By placing one component per layout manager and giving each layout manager the same android: layout_weight value of 1 via the wrapCenterWeight1 style at lines 9 and 16, we space out the components evenly. The wrapCenterWeight1 style, which also sets android:layout_width and android:layout_height to wrap_content and android:gravity to center, is defined in the styles.xml file (lines 11–16), shown in EXAMPLE 9.9. The use of styles simplifies the coding of our layout XML files.

EXAMPLE 9.7 The fragment_game_control.xml file, Hangman app, Version 1

We give a background color defined in colors.xml to the outer LinearLayout (line 6). The button is styled at line 12 using the buttonStyle style, defined in Example 9.9 at lines 31–33. ButtonStyle inherits from textStyle, defined at lines 18–23 in Example 9.9.

The TextView element (lines 17–19) is styled with the textStyle style at line 19. The TextView element stores how many guesses the user has left. We retrieve that value from the Model. The button, whose text says PLAY (line 11—the value of the play String in the strings.xml file—line 3 of EXAMPLE 9.8), triggers execution of the play method when the user clicks on it (line 13).

EXAMPLE 9.8 The strings.xml file, Hangman app, Version 1

EXAMPLE 9.9 The styles.xml file, Hangman app, Version 1

We keep the MainActivity class, shown in EXAMPLE 9.10, to a minimum. It includes a Hangman instance variable representing the Model at line 10. Since the onCreate method can be called several times during the life cycle of this activity, we only instantiate it (line 16) if it has not been instantiated before (i.e., if it is null). We call the getGuessesLeft method of the Hangman class with it at line 19 in order to set the text inside the TextView of the fragment. We retrieve that TextView at line 18 using the findViewById method. The class also includes the play method (lines 22–23), which is implemented as a do-nothing method at this point (if we do not implement it, if we run and click on the button, the app will stop working).

EXAMPLE 9.10 The MainActivity class, Hangman app, Version 1

FIGURE 9.7 The Hangman app, Version 1, running in horizontal orientation

FIGURE 9.7 shows the app, Version 1, running in horizontal orientation.

9.5 Defining a Fragment Using a Layout XML File and Adding the Fragment to an Activity by Code, Hangman, App Version 2

In Version 2, we display the fragment on the top right pane (with the blue background) of the screen showing the state of completion of the word, and an EditTextfor the user to enter a letter. We code the fragment in an XML file, fragment_game_state.xml, and create the fragment by code.

Furthermore, in order to better focus on fragments and keep things simple, we run the app in horizontal orientation only by adding this line to the AndroidManifest.xml file inside the activity element:

android:screenOrientation=”landscape”

EXAMPLE 9.11 shows the fragment_game_state.xml file. It includes a TextView, which shows the state of completion of the word and an EditText, where the user can enter a letter. We give ids to these two elements at lines 11 and 17 so that we can later retrieve them using the findViewById method. They are styled with textSyle and editTextStyle, which are both defined in the styles.xml file shown in Example 9.9. Like in Example 9.7 for the left pane fragment, we wrap each element around a LinearLayout element styled with the wrapCenterWeight1 style, so that the elements are centered and spaced evenly.

EXAMPLE 9.11 The fragment_game_state.xml file, Hangman app, Version 2

We want to close the soft keyboard when the user touches the Done key. In order to do that, we set the value of the android:imeOptions attribute, inherited from TextView, to actionDone at line 19. IME stands for Input Method Editor, and it enables the user to enter text. It is not restricted to a keyboard—the user can also enter text by voice. The attribute android:imeOptions enables us to specify the options that we want to enable.

We need to show the game state fragment when the app starts. We first need to create the fragment and add it to the activity, then set the text inside the TextView element of the fragment. For that, we can call the currentIncompleteWord method of our Model. To incorporate a fragment into an activity, we need to implement the following steps:

  • ▸ Get a reference to the fragment manager for this Activity.

  • ▸ Create a fragment transaction.

  • ▸ Create a fragment.

  • ▸ Have the fragment transaction add the fragment to a container (a ViewGroup).

  • ▸ Commit the fragment transaction.

The FragmentManager abstract class provides the functionality to interact with fragments within an activity. A reference to the FragmentManager for the current Activity can be obtained by calling the getFragmentManager method, shown in TABLE 9.3, with the Activity reference. TABLE 9.4 lists some methods of the FragmentManager class.

TABLE 9.3 The getFragmentManager method of the Activity class

Method Description
FragmentManager getFragmentManager( ) Returns the FragmentManager for this Activity.

TABLE 9.4 Selected methods of the FragmentManager class

Method Description
FragmentTransaction beginTransaction( ) Starts a series of operations for this fragment manager. Returns a FragmentTransaction.
Fragment findFragmentById( int id ) Returns a Fragment identified by its id.
Fragment findFragmentByTag( String tag ) Returns a Fragment identified by its tag.

Thus, we can obtain a FragmentManager reference for the current Activity using:

FragmentManager fragmentManager = getFragmentManager();

To obtain a FragmentTransaction for the preceding FragmentManager, we write:

FragmentTransaction transaction = fragmentManager.beginTransaction( );

The FragmentTransaction class provides methods (see TABLE 9.5) to perform fragment operations, such as adding a fragment to an activity, removing it, replacing it, hiding it, showing it, etc. These methods return the FragmentTransaction that calls them, so that we can chain method calls. There are other methods, for example, to define animations or transitions when a fragment transaction is being performed. It is an abstract class so we cannot instantiate a FragmentTransaction object with a constructor. We can use the beginTransaction method of the FragmentManager class to instantiate a FragmentTransaction object.

After having obtained a FragmentTransaction named transaction for an Activity, we can add a GameStateFragment whose id is game_state to that Activity using the following code:

GameStateFragment fragment = new GameStateFragment( );
transaction.add( R.id.game_state, fragment );
transaction.commit( );

When performing a fragment transaction, for example, adding or removing a fragment to or from an activity, we can add it to the back stack of the activity by calling the addToBackStack method before calling the commit method. The back stack keeps track of the history of these fragment transactions within an activity so that the user can revert to a prior fragment by pressing the Back button of the device.

TABLE 9.5 Selected methods of the FragmentTransaction class

Method Description
FragmentTransaction add( int containerViewId, Fragment fragment, String tag ) Adds a fragment to an activity. ContainerViewId is the resource id of the container that the fragment will be placed in; fragment is the fragment to be added. Tag is an optional tag name for the fragment. The added fragment can later be retrieved by the fragment manager using the findFragmentByTag method. Returns the FragmentTransaction reference calling this method; thus, method calls can be chained.
FragmentTransaction add( int containerViewId, Fragment fragment ) Calls the method above with a null tag parameter.
FragmentTransaction add( Fragment fragment, String tag ) Calls the method above with a resource id parameter equal to 0.
FragmentTransaction hide( Fragment fragment ) Hides fragment (i.e., hides its View).
FragmentTransaction show( Fragment fragment ) Shows fragment (i.e., shows its View if it had been hidden before—the default is that the fragment is shown).
FragmentTransaction remove( Fragment fragment ) Remove fragment from its activity.
FragmentTransaction replace( int containerViewId, Fragment fragment, String tag ) Similar to the first add method, but instead replaces an existing fragment inside the container whose id is containerViewId.
FragmentTransaction replace( int containerViewId, Fragment fragment ) Calls the method above with a null tag parameter.
int commit( ) Schedules that fragment transaction to be committed.
FragmentTransaction addToBackStack( String name ) Adds this transaction to the back stack of the activity; name is optional and stores the name of that back stack state.

EXAMPLE 9.12 shows the updated MainActivity class. When the app starts, the onCreate method executes, and we create the fragment and attach it to the activity. At line 23, we get a reference to the fragment manager for this activity by calling the getFragmentManager method from the Activity class. We only want to create a new GameStateFragment and add it to the activity if it has not been created yet. Using the findFragmentById method at line 24, we check if there is already a fragment with the id game_state within this activity. If there is, we do nothing. If there is not, we create one and add it to the activity at lines 25–28.

EXAMPLE 9.12 The MainActivity class, Hangman app, Version 2

At line 25, using the FragmentManager reference, we get a FragmentTransaction reference, transaction. At line 26, we define and instantiate a GameStateFragment reference, fragment. At line 27, we add fragment to this Activity, placing it inside the ViewGroup (a LinearLayout in this case) whose resource id is game_state. Finally, we commit the fragment transaction at line 28. At this point, we still need to place the text of the puzzle inside the TextView of the puzzle fragment that we just added to the activity. In order to do that, we first need to access the View of the fragment and use the findViewById method, passing the id of the TextView as its argument. However, at this point the View of the fragment is null, so attempting to use it would result in a NullPointerException, crashing the app. Thus, we do this inside the GameStateFragment class. Thus, in the GameStateFragment class, we need to access the value of the current incomplete word of the hangman game. To that end, we provide the getGame method (lines 32–34) in the MainActivity class. From the GameStateFragment class, we can access the activity by calling the getActivity method (see TABLE 9.6), inherited from the Fragment class. With a MainActivity reference, we can access the game by calling getGame, and then access the incomplete word as follows:

TABLE 9.6 The getActivity and getView methods of the Fragment class

Method Description
Activity getActivity( ) Returns the activity that this fragment belongs to.
View getView( ) Returns the root View for this fragment.
MainActivity fragmentActivity = ( MainActivity ) getActivity( );
String currentWord = fragmentActivity.getGame( ).currentIncompleteWord( );

EXAMPLE 9.13 shows the GameStateFragment class. Since it inherits from Fragment, we provide a mandatory default constructor at lines 12–13. We inflate the fragment_game_state.xml file at lines 18–19 inside the onCreateView method. When the fragment transaction is committed at line 28 of Example 9.12, the constructor, the onAttach, onCreate, onCreateView, onActivityCreated, and onStart methods of the Fragment life cycle are called in that order. When the onStart method (lines 22–30) executes, the fragment’s View and the TextView inside it have been instantiated. Thus, we can set the text inside the TextView at that point.

We get a reference to the fragment’s View at line 24 using the getView method (shown in Table 9.6), inherited from the Fragment class. With it, we can call the findViewById method to access any View inside it that has been given an id. At lines 25–26, we call it and assign the TextView whose id is state_of_game to gameStateTV. We get a reference to the parent Activity of this fragment at line 27, casting it to a MainActivity. We chain method calls to getGame and currentIncompleteWord in order to retrieve the game’s incomplete word and assign the resulting value to the text of gameStateTV at lines 28–29.

EXAMPLE 9.13 The GameStateFragment class, Hangman app, Version 2

FIGURE 9.8 The Hangman app, Version 2, running in horizontal position, after the user enters a letter and hits the Done (or Check) button on the soft keyboard

FIGURE 9.8 shows the app running in the emulator after the user enters the letter U and hits the Done button on the soft keyboard.

9.6 Defining and Adding a Fragment to an Activity by Code, Hangman, App Version 3

In Version 3, we display the fragment on the bottom right pane (with the green background) of the screen showing a message about the result of the game in a TextView. This time, we do not use an XML file to define the fragment, we define and create it entirely by code.

Defining the fragment’s Graphical User Interface (GUI) is similar to defining a View by code instead of using an XML file. We do this in the GameResultFragment class. Creating the fragment is identical to creating a game state fragment. It takes place in the MainActivity class.

EXAMPLE 9.14 shows the updated MainActivity class. It is very similar to its previous version shown in Example 9.12. We add a GameResultFragment in the LinearLayout whose id is game_result (lines 31–36).

In the GameResultFragment class, we define the fragment by code. This fragment only has a TextView in it. Furthermore, we need to access that TextView to set its text when the game ends. Rather than giving the TextView an id when we create it, we declare it as an instance variable so that we have a direct reference to it inside the GameResultFragment class (line 12 of EXAMPLE 9.15). The onCreateView method, at lines 14–20, calls the setUpFragmentGui method at line 17 in order to create the GUI for this fragment. It returns the View returned by its super method at lines 18–19. The setUpFragmentGui method is coded at lines 22–28. Since it is possible that onCreateView is called several times during the life cycle of the fragment, we can in turn expect setUpFragmentGui to be called several times too. Thus, we want to instantiate gameResultTV (line 24) and add it to the ViewGroup that is the root View of the fragment (line 26) only once, when it is null (line 23). We center the text inside gameResultTV at line 25.

EXAMPLE 9.14 The MainActivity class, Hangman app, Version 3

EXAMPLE 9.15 The GameResultFragment class, Hangman app, Version 3

In the onStart method (lines 30–33), which is automatically called after onCreateView and onActivityCreated, we hard code the text inside gameResultTV to “GOOD LUCK” (line 32).

We need to center the TextView of GameResultFragment within its parent LinearLayout. Inside the activity_main.xml file, we add the following attribute-value pair to the last LinearLayout element (the one whose id is game_result and is located in the right bottom pane):

android:gravity=”center”

FIGURE 9.9 The Hangman app, Version 3, running in horizontal position

FIGURE 9.9 shows the app running in the emulator. All three fragments are now present:

  • ▸ The left pane (game control) fragment in red is defined and created using XML.

  • ▸ The top right pane (game status) fragment in blue is defined in XML and created by code.

  • ▸ The bottom right pane (game result) fragment in green is defined and created by code.

9.7 Communication between Fragments and Their Activity: Enabling Play, Hangman, App Version 4

We have now created an activity with three fragments inside, defined and created in different ways. In Version 4, we complete the app by processing the user’s letter and enabling the user to play the game. We also illustrate how fragments can communicate with their activity when processing an event.

The user enters a letter in the game state fragment, then clicks on the PLAY button on the left pane, which triggers execution of the play method inside the MainActivity class. To enable play, we do the following:

  • ▸ Capture the letter entered by the user.

  • ▸ Call the guess method of Hangman.

  • ▸ Update the number of guesses left in the TextView inside the left pane.

  • ▸ Update the incomplete word in the TextView inside the top right pane.

  • ▸ Clear the EditText.

  • ▸ If the game is over, update the message in the TextView inside the bottom right pane and clear the hint in the EditText.

EXAMPLE 9.16 shows the play method. We retrieve the EditText using its id at line 46 and assign it to the variable input. At line 47, we get an Editable reference for input. If it is not null and contains at least one character (line 48), we process the first character as the user’s play. Otherwise, we do nothing.

EXAMPLE 9.16 The play method of the MainActivity class, Hangman app, Version 4

At lines 49–53, we play and update the number of guesses left. Since we gave the TextView that displays the number of guesses left an id (status) in the fragment_game_control.xml file, we retrieve it using the findViewById method at line 52. We call getGuessesLeft with game at line 53 to retrieve the number of guesses left and display it inside the TextView.

At lines 55–62, we update the state of the incomplete word. We first get a reference to the GameStateFragment at lines 56–58, then get a reference to the fragment’s View at line 59. With it, we call findViewById at lines 60–61 in order to get a reference to the TextView whose id is state_of_game. We call the currentIncompleteWord of Hangman to set the text of that TextView at line 62. At lines 64–65, we clear the EditText.

We then check if the game is over at line 67. If it is not, we do nothing. If it is (line 68), we clear the hint in the EditText (lines 78–79), and we tell the user the result of the game. In order to do this, we need to access the TextView inside the GameResultFragment; however, that TextView does not have an id. Thus, in order to access it, we get a reference to the GameResultFragment that contains it (lines 69–70) with the FragmentManager and call setResult, a new method of the GameResultFragment class, with it (lines 72–76).

EXAMPLE 9.17 shows the setResult method of the GameResultFragment class. The rest of the class is unchanged. It sets the text of the gameResultTV instance variable.

EXAMPLE 9.17 The setResult Method of the GameResultFragment class, Hangman app, Version 4

Figure 9.1, at the beginning of the chapter, shows the app running at some point in the game. FIGURE 9.10 shows the app running after the user won. The TextView that was previously showing either a hint or a letter is no longer visible because its contents are empty and its android: layout_width and android:layout_height attributes are set to wrap_content.

FIGURE 9.10 The Hangman app, Version 4, running in horizontal position, after the user won

9.8 Using an Invisible Fragment, Hangman, App Version 5

We can also use a fragment that can perform some work in the background of the app, without any visual representation. For this, we use the add method of the FragmentTransaction class that does not have a View id parameter. If we want to be able to retrieve the fragment later, we should provide a tag name. Such a fragment could perform some work in the background of the app while the user is allowed to interact with the app. It could, for example, retrieve some data from a file, a database, a remote URL, or possibly use the device’s GPS to retrieve some live location data.

To demonstrate how to use such a fragment, we keep our example to a minimum. Our fragment is hard coded and includes the warning method. We will give a warning to the user when he or she has only one guess left and display the String returned by the warning method. That information normally comes from the Model, but in the interest of keeping things as simple as possible, we hard code it in this class. EXAMPLE 9.18 shows the BackgroundFragment class. Because this fragment is not visible, the onCreateView method is not called. Thus, we did not code it. At lines 10–12, we code the warning method, which we call from the MainActivity class.

EXAMPLE 9.18 The BackgroundFragment class, Hangman app, Version 5

EXAMPLE 9.19 shows parts of the updated MainActivity class. We create an invisible BackgroundFragment in the onCreate method at lines 40–45, without waiting for the user to interact with the app. This strategy can be used in apps where we need to bring data from an outside source. At line 43, we use the add method of the FragmentTransaction class with a Fragment and a String parameter. The String represents a tag name for the fragment, and later we can retrieve that fragment via its tag name, background, using the findFragmentByTag method of the FragmentManager class. We use the fragment in the play method (lines 52–99). If the user has only one guess left (line 75), we display a warning inside the TextView of the GameResultFragment. We retrieve the BackgroundFragment based on its tag at lines 76–77 and the GameResultFragment based on its id at lines 78–79. We then call setResult with the GameResultFragment, passing the warning retrieved by calling the warning method with the BackgroundFragment (line 81).

EXAMPLE 9.19 The MainActivity class, Hangman app, Version 5

FIGURE 9.11 shows the app running in the emulator. The user only has one guess left.

FIGURE 9.11 The Hangman app, Version 5, running in horizontal position—the user only has one guess left

9.9 Making a Fragment Reusable, Hangman, App Version 6

Version 5 of the app works, but the GameStateFragment class is not really reusable. For example, at line 27 of Example 9.13, the use of the MainActivity reference and the call to getGame at line 28 inside the GameStateFragment class assume that the MainActivity class exists and includes the method getGame.

EXAMPLE 9.20 shows a better implementation, making the GameStateFragment class more reusable. Inside the fragment class, we use an inner interface, which we name Callbacks (lines 15–17), and transfer all the method calls using a MainActivity reference inside the fragment class to the MainActivity class via that interface. Inside the fragment class, whenever we call a MainActivity method, we add a method to the Callbacks interface and call that method from the fragment class. The MainActivity class implements the interface and thus must override that method. In this way, we effectively transfer the MainActivity method calls from the fragment class to the MainActivity class.

EXAMPLE 9.20 The reusable GameStateFragment class, Hangman app, Version 6

Another reusability issue is that the method getGame returns a Hangman reference. Thus, since getGame is in the Callbacks interface, which is inside the GameStateFragment class, our GameStateFragment class is only reusable with the Hangman class. To solve that problem, we create an interface, which we name WordGame, and make getGame return a WordGame reference instead. The WordGame interface only has one method, currentIncompleteWord. This is needed because we call that method inside the GameStateFragment class with the WordGame reference returned by getGame. Thus, the GameStateFragment class is now reusable with any activity that implements the Callbacks interface and with any class that implements the WordGame interface, which are both very simple and generic. In this app, we modify Hangman so that it implements WordGame.

Here is how that method transfer mechanism is implemented in the GameStateFragment class.

Before: Inside GameStateFragment (old implementation), from Example 9.13:

After: Inside GameStateFragment (new implementation), from Example 9.20:

The call to getGameFromActivity at line 40 triggers a call by the mCallbacks instance variable, declared and initialized at line 13, to the getGame method of the Callbacks interface (declared at line 16), which will execute inside a class implementing the Callbacks interface and, therefore, implementing getGame.

At lines 19–23, we declare and instantiate sDummyCallbacks, an object of an anonymous class implementing the Callbacks interface. That anonymous class overrides the getGame method, returning null, at lines 20–22; sDummyCallbacks is assigned to the instance variable mCallbacks of type Callbacks at line 13.

The mCallbacks instance variable is meant to be a reference to the activity that this fragment belongs to. The onAttach method, inherited from the Fragment class, is overridden at lines 44–51. This method is called when this fragment is attached to an activity. Furthermore, when that method is called, we expect its parameter, context, to be an instance of a superclass of Context (most likely a superclass of Activity) that implements the Callbacks interface. If it is not (line 46), we throw an IllegalStateException at lines 47–48. If it is, the context parameter is typecast to Callbacks and is assigned to mCallbacks at line 50.

In this new implementation, there is no mention of the MainActivity class, and there is no mention of a method from the MainActivity class named getGame. The only requirement is that the MainActivity class, or any activity class that we use with the GameStateFragment class, implements the Callbacks interface. This, in turn, forces MainActivity, or any class that implements the Callbacks interface, to override the getGame method. Note that in this example, there is only one method in the Callbacks interface, but more generally, there could be several.

The onDetach method, inherited from the Fragment class, is overridden at lines 53–56. That method is called when this fragment is detached from its activity. It reassigns the default Callbacks object sDummyCallbacks to mCallbacks at line 55.

EXAMPLE 9.21 shows the class header of the updated MainActivity class, which now implements the Callbacks inner interface of GameStateFragment. There is no other change as compared to Example 9.19: the getGame method was already implemented before and is still the same, but it now overrides the getGame method of the Callbacks interface.

EXAMPLE 9.21 The class header of the MainActivity class, Hangman app, Version 6

EXAMPLE 9.22 shows the WordGame interface, which declares the currentIncompleteWord method (line 4).

EXAMPLE 9.22 The WordGame interface, Hangman app, Version 6

EXAMPLE 9.23 shows the class header of the updated Hangman class, which now implements the WordGame interface. There is no other change as compared to Example 9.1: the currentIncompleteWord method was already implemented before and is still the same, but it now overrides the currentIncompleteWord method of the WordGame interface.

EXAMPLE 9.23 The class header of the Hangman class, Hangman app, Version 6

Note that we implemented Callbacks as an inner interface of GameStateFragment and WordGame as a separate interface. Either can be implemented as an inner interface or as a separate interface.

9.10 Improving the GUI: Processing the Keyboard Input Directly, Hangman, App Version 7

Version 6 works but it is annoying to have to click on the PLAY button every time. In Version 7, we enable play as soon as the user closes the keyboard. We can either eliminate or convert the PLAY button to a “Play Another Game” button, which you can do in the exercises.

The EditText is located inside the game state fragment, so event handling for Version 7 takes place in that fragment. OnEditorActionListener, a public static inner interface of the TextView class, provides the functionality to handle a key event inside an EditText. TABLE 9.7 lists its only method, onEditorAction.

As always, in order to set up event handling, we do the following:

  1. Write an event handler (a class extending a listener interface).

  2. Instantiate an object of that class.

  3. Register that object listener on one or more GUI components.

EXAMPLE 9.24 shows the updated GameStateFragment class. At lines 79–94, we define the private class OnEditorHandler, which implements the TextView.OnEditorActionListener interface. It overrides its only method, onEditorAction, at lines 80–93. It does the following:

TABLE 9.7 The onEditorAction method of the TextView.OnEditorActionListener interface

Method Description
boolean onEditorAction( TextView view, int keyCode, KeyEvent event ) view is the TextView that was clicked; keyCode is an integer identifying the key that was pressed; event is the Key event.

EXAMPLE 9.24 The GameStateFragment class, Hangman app, Version 7

  • ▸ Hides the keyboard (lines 82–87).

  • ▸ Plays (lines 89–90).

Since we already have a play method in the MainActivity class, we can reuse its code. The existing play method accepts a View parameter, expected to be a Button, which the method does not actually use. We could call it and pass null, but it is not good practice to pass null to a method from another class, since in theory (and in general), it could use that parameter to call methods. Thus, we create an additional play method in the MainActivity class (see EXAMPLE 9.25) that accepts no parameter and does exactly the same thing as the existing play method (which we can leave as a do-nothing method—if we choose to delete it, we also need to delete the android: onClick attribute in the fragment_game_control.xml file). We use the same strategy as in Version 6 to transfer the call to the play method of MainActivity via a play method of the Callbacks interface (declared at line 20), which we call with the fragment’s play method (lines 71–73). Thus, the call to the fragment’s play method at line 90 triggers a call to the Callbacks’ play method. In this way, we keep our fragment class reusable (with a class implementing the Callbacks interface).

In order to close a keyboard by code, we can use an InputMethodManager reference. The InputMethodManager class manages the interaction between an application and its current input method. That class does not have a constructor but we can obtain an InputMethodManager reference by calling the getSystemService of the Context class using an Activity reference (which is a Context reference since Activity inherits from Context).

The getSystemService method has the following API:

Object getSystemService( String nameOfService )

Its String parameter represents a service, and TABLE 9.8 lists some of the possible values. Depending on the service specified, it returns a manager object reference for that service. For example, if we specify LOCATION_SERVICE, it returns a LocationManager reference that we can use to gather location data from the device’s GPS system. If we specify WIFI_SERVICE, it returns a WifiManager reference that we can use to access data regarding the device’s Wi-Fi connectivity. Because that method returns a generic Object, we need to typecast the returned object reference to what we expect. We want to obtain an InputMethodManager reference, so we pass the argument Context.INPUT_METHOD_SERVICE and cast the resulting object reference to an InputMethodManager as follows (and as in lines 83–84):

InputMethodManager inputManager = ( InputMethodManager )
getActivity( ).getSystemService( Context.INPUT_METHOD_SERVICE );

To close the keyboard, we use one of the hideSoftInputFromWindow methods of the InputMethodManager class, shown in TABLE 9.9. The first parameter has type IBinder, an interface that encapsulates a remotable object. A remotable object is an object that can be accessed outside its context via a proxy. We want to pass to hideSoftInputFromWindow an IBinder reference that represents the window that the current View (in this case the EditText) is attached to. We call the getCurrentFocus method of the Activity class with the current Activity object. It returns the View that has the focus in the current window. Using that View, we call the get WindowToken of the View class. It returns a unique token that identifies the window that this View is attached to. At line 86, we chain these two method calls using the expression:

getActivity( ).getCurrentFocus( ).getWindowToken( )

TABLE 9.8 Selected String constants and their values, as well as the corresponding return type of the getSystemService method of the Context class when using one of those constants as the method’s parameter

String Constant String Value getSystemService Return Type
POWER_SERVICE power PowerManager
LOCATION_SERVICE location LocationManager
WIFI_SERVICE wifi WifiManager
DOWNLOAD_SERVICE download DownloadManager
INPUT_METHOD_SERVICE input_method InputMethodManager

TABLE 9.9 Selected hideSoftInputFromWindow method of the InputMethodManager class

Method Description
boolean hideSoftInputFromWindow( IBinder token, int flags ) token is the token of the Window making the request as returned by the getWindowToken method of the View class; flags specifies additional condition for hiding the soft input.

TABLE 9.10 shows the two constants of the InputMethodManager class that can be used as values for the second parameter of the hideSoftInputFromWindow method. Since the user explicitly opens the keyboard when trying to enter some input, we should not use the HIDE_IMPLICIT_ONLY constant in this case. We use HIDE_NOT_ALWAYS, so the second argument of we method call at line 87 is:

InputMethodManager.HIDE_NOT_ALWAYS

When we run the app, play happens as soon as the keyboard closes. We no longer need to click on the PLAY button.

TABLE 9.10 Selected int constants of the InputMethodManager class to be used as the second parameter of hideSoftInputFromWindow

Constant Value Meaning
HIDE_IMPLICIT_ONLY 1 The soft input window should only be hidden if it was not explicitly open by the user.
HIDE_NOT_ALWAYS 2 The soft input window should normally be hidden unless it was originally forced open by code.

EXAMPLE 9.25 Changes in the MainActivity class, Hangman app, Version 7

Chapter Summary

  • Fragments were introduced in API level 11 to provide better support for large screen devices such as tablets.

  • A fragment is a portion of an activity, and can help manage a portion of the screen. We can think of a fragment as a mini-activity within an activity.

  • There can be several fragments within one activity.

  • A fragment can have a user interface or simply perform some work in the background of an activity.

  • A fragment has its own life-cycle methods but depends on its activity running.

  • An activity can use the getFragmentManager method to access its fragment manager. With it, it can access and manage its fragments.

  • A fragment can access its activity using the getActivity method.

  • A fragment can be defined, created, and added to an activity using XML only, code only, or a mix of both.

  • When creating and adding a fragment to an activity, we use the FragmentTransaction class. When adding a fragment, we can identify it with an id, a tag, or both.

  • A fragment manager can find fragments either by id or by tag.

  • A fragment can be reused in several activities.

  • A fragment class should be coded so that it is reusable and not tied to a particular activity class. We can do this by using an inner interface inside the fragment class.

Exercises, Problems, and Projects

Multiple-Choice Exercises

  1. Fragments were introduced with API level

    • 2

    • 8

    • 11

    • 13

  2. The direct superclass of Fragment is

    • Content

    • Activity

    • Object

    • FragmentActivity

  3. How many fragments can there be in one activity?

    • 0

    • 1 only

    • 0 or more

  4. What method of the following is not a life-cycle method of a fragment?

    • onStart

    • onCreate

    • onRestart

    • onStop

  5. What method can be used by a fragment to access its activity?

    • activity

    • getActivity

    • getFragment

    • findActivity

  6. What class can be used by an activity to access its fragments?

    • FragmentTransaction

    • Activity

    • FragmentManager

    • Manager

  7. What method of the FragmentTransaction class is used to add a fragment to an activity?

    • add

    • addFragment

    • insert

    • insertFragment

  8. An activity can retrieve one of its fragment via its

    • tag only

    • id only

    • tag or id

    • it cannot

  9. We can make a fragment class reusable by transferring all the method calls by its activity inside the fragment class to the activity class via

    • an inner class

    • an interface

    • it is not possible

Fill in the Code

  1. Inside an activity class, retrieve the fragment manager.

  2. Inside an activity class, add a fragment of type MyFragment and place it inside a ViewGroup element whose id is my_id.

    // fm is a FragmentManager reference
    FragmentTransaction transaction = fm.beginTransaction( );
    // Your code goes here
    
  3. Inside an activity class, add a fragment of type MyFragment and place it inside a ViewGroup element whose id is my_id. Give it the tag my_tag.

    // fm is a FragmentManager reference
    FragmentTransaction transaction = fm.beginTransaction( );
    // Your code goes here
    
  4. Inside an activity class, add a fragment of type MyFragment that does not have a user interface. Give it the tag my_tag.

    // fm is a FragmentManager reference
    FragmentTransaction transaction = fm.beginTransaction( );
    // Your code goes here
    
  5. Inside a fragment class, write the code so that the fragment is inflated from the my_fragment.xml file.

    public View onCreateView( LayoutInflater inflater, ViewGroup
           container, Bundle savedInstanceState ) {
       // Your code goes here
    }
    
  6. Inside a fragment class, write the code to retrieve the activity that this fragment belongs to. Assume that the activity type is MyActivity.

    // Your code goes here
    
  7. The following fragment class is not reusable. Change it so that it is.

    package com.you.myapp;
    import android.app.Fragment;
    public class MyFragment extends Fragment {
      public MyFragment( ) {
      }
      public void onStart( ) {
        super.onStart( );
        MyActivity activity = ( MyActivity ) getActivity( );
        activity.update( );
      }
    }
    // Your code goes here
    

Write an App

  1. Write a mini app containing one activity using two fragments as shown below:

  2. Write a mini app containing one activity using three fragments as shown below:

  3. Write a mini app containing one activity using four fragments as shown below:

  4. Write an app containing one activity using two fragments (one left and one right) as follows:

    The left fragment contains a button and the right fragment contains a label that simulates a traffic light, red when we start. When the user clicks on the button, the label changes to green. When the user clicks on the button again, the label changes to yellow. When the user clicks on the button again, the label changes to red, and so on. Even though the functionality is simple, you should include a Model for this app.

  5. Write an app containing one activity using several fragments (one at the top and one or more below) as follows:

    The top fragment should occupy one fourth of the screen and contain three radio buttons, showing 1, 2, and 3. When the user selects one of them, the bottom of the screen displays the corresponding number of fragments (use a different color for each), and they all should occupy the same amount of space. For example, if the user clicks on the radio button that says 2, then two fragments of equal height are displayed at the bottom of the screen.

  6. Write an app containing one activity using two fragments (one on top and one at the bottom) as follows:

    The app simulates a flashlight. The top fragment comprises 20% of the screen and contains an on and off switch. You can use the Switch class for this. The bottom fragment comprises 80% of the screen and is black. When the user turns the switch on, the bottom fragment turns yellow. When the user turns the switch off, the bottom fragment reverts back to black. Even though the functionality is simple, you should include a Model for this app.

  7. Write an app containing one activity using two fragments (one on top and one at the bottom) as follows:

    The app simulates a dimmer. The top fragment comprises 20% of the screen and contains a slider. You can use the SeekBar class for this. The bottom fragment comprises 80% of the screen and is yellow. When the user moves the slider, the yellow fades. You can assume that the dimmer slider controls the transparency of the yellow color. Even though the functionality is simple, you should include a Model for this app.

  8. Write an app containing one activity using two fragments (one on the left and one on the right) as follows:

    The app simulates a traffic light. The left fragment contains a button and the right fragment contains three labels, the top one is red when we start and the others are transparent. When the user clicks on the button, the top label becomes transparent and the bottom label changes to green. When the user clicks on the button again, the bottom label becomes transparent and the middle label changes to yellow. When the user clicks on the button again, the middle label becomes transparent and the top label changes to red, and so on. Even though the functionality is simple, you should include a Model for this app.

  9. Write an app containing one activity using two fragments (one at the top and one below) as follows:

    The top fragment should occupy one fourth of the screen and contains a button, the bottom fragment three fourths of the screen. When the user clicks on the button, a tic-tac-toe game shows up inside the bottom fragment. Play should be enabled. You should include a Model for the tic-tac-toe game.

  10. Write an app containing one activity using two fragments (one at the top occupying 75% of the space and one below occupying 25% of the space) as follows:

    The app generates a random number between 1 and 5 and the user needs to guess it. The bottom fragment shows an EditText asking the user to enter a number. The top fragment shows some feedback to the user (for example, “You won” or “You lost”). Even though the functionality is simple, you should include a Model for this app.

  11. Write the same app as in exercise 26 but add a third fragment, this one invisible: it provides the functionality of generating a random number.

  12. Write an app that improves this chapter’s app. On the left pane, add another fragment that contains a button so that the user can play another game by clicking on it.

  13. Write the same app as in exercise 28 but add a file that stores a large number (1,000 or more) of potential words. The Hangman constructor should pick one at random from the file. Add an invisible fragment to instantiate a Hangman object and start the game with the word selected.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.78.47