Chapter 2. Fragments and UI Flexibility

This chapter builds on the concepts introduced in the previous chapter to provide solutions to addressing specific differences in device layouts. The chapter explains the use of adaptive Activity layout definitions to create apps that automatically rearrange their user interface in response to differences in device form factors. With adaptive Activity layout definitions, applications are able to support a wide variety of devices using just a few properly designed fragments.

In this chapter, we will cover the following topics:

By the end of this chapter, we will be able to implement a user interface that uses fragments to automatically adapt to differences in device layouts and coordinates user actions across the involved fragments.

Utilizing fragments in our user interface design provides a good foundation for creating applications that more easily adapt to device differences, but we must go a little further to create truly flexible UIs. We must design our application such that the fragments that make up the UI are easily rearranged in response to the characteristics of the device on which the app is currently running.

To achieve this, we must use some techniques to dynamically change the layout of individual fragments in response to the current device's characteristics. Once we employ such a technique, we must be sure that we implement our fragments in such a way that each fragment is able to function effectively independent of layout changes that might affect the behavior or even existence of other fragments within the activity.

As we mentioned in the previous section, creating a flexible UI requires that the layout and positioning of fragments within an activity must be able to change in response to differences in device characteristics. We can include code in our application to dynamically arrange fragments in response to the form factor of the device on which our app is running, but in most cases, doing so is not only unnecessary but also undesirable. The deeper the dependencies between the user interface and application code, the more difficult maintaining and enhancing an application becomes. Although there will always be some degree of dependency between our user interface and application code, we want to minimize such dependencies and instead do as much of our user interface layout-related work within layout resources as possible.

The easiest way to build flexibility into our application user interface is to take advantage of the Android resource system's built-in device adaptability. Android allows us to design different layout-related resources for our application with each optimized for and associated with a specific set of device characteristics. At runtime, the Android resource system takes care of automatically selecting and loading the appropriate resources for the current device. Although this feature can be used to dynamically modify the layout of any activity, we'll see that it is particularly effective when used in conjunction with fragments.

To see Android resource selection in action, let's continue with our application from the previous chapter. As you'll recall, the layout for our activity is in the activity_main.xml resource file and looks like this:

This layout stacks our fragments, BookListFragment and BookDescFragment, one on top of the other. Although that layout renders well on a smartphone held vertically in the portrait orientation, rotating the phone so that it's held horizontally in the landscape orientation creates a much less attractive appearance as seen here:

Dynamic fragment layout selection

The current layout is clearly not making the best use of the available screen space in this orientation. When the phone is orientated in landscape, the application would look much better if we position the two fragments side-by-side.

We can add support for an alternative layout to our application by creating a new resource file with the fragments appropriately arranged. To create the resource file, we first add another folder under the res folder of the project tree called layout-land.

To create the new folder in Android Studio, perform the following steps:

The New Resource Directory dialog will appear similar to the following screenshot:

Adding an alternate layout resource

Now copy the activity_main.xml resource file from the layout resource folder to the layout-land resource folder. We now have two copies of the activity_main.xml resource file as shown in the following screenshot:

Adding an alternate layout resource

We can now modify the activity_main.xml resource file in the layout-land folder to arrange the fragments to render properly when the phone is in landscape orientation. First, we switch the LinearLayout element from a vertical to a horizontal orientation. We then change the layout_width values for each fragment to 0dp and the layout_height values to match_parent. We can leave each of the fragment's layout_weight value as 1 so that LinearLayout spaces them equally left to right.

The updated resource file looks like this:

Having done nothing more than adding this simple resource file to our project, the application now displays the list of titles and book description next to one another when run on a device held in a landscape orientation as shown in the following screenshot:

Adding an alternate layout resource

During runtime, when the MainActivity class loads the R.layout.activity_main resource, the Android resource system returns the appropriate version of the activity_main.xml resource file for that orientation. When the user rotates the device to a different orientation, Android automatically recreates the activity and loads the appropriate resource for the new orientation.

The Android environment detects a wide variety of device form factor characteristics. By taking advantage of fragments, we are able to create an application that easily adapts to device differences by simply providing different layout resource files that shift around the location of our fragments as if they are puzzle pieces.

Without fragments, we would've had to provide the entire layout for the activity, radio buttons, text views, everything, in both of the layout files. We would then find ourselves having to maintain two complex, almost identical files. By using fragments, the individual pieces are self-contained and non-duplicated. Fragments modify the layout in an easy manner and simplify our application maintenance.

The same technique we use to adapt our user interface to device orientation differences can be taken much further to work with differences in screen size. The Android resource system has device screen size awareness and therefore supports creating corresponding resource folders. The resource selection can be based on general screen size groups or specific screen size constraints.

Differences in device screen size are one of the most common reasons for using layout resources to manage fragments. With this being the case, understanding how to use layout resources to deal with differences in screen size is essential to working effectively with fragments.

The configuration information of each Android device includes the screen size group to which the device belongs. The four screen size groups are small, normal, large, or xlarge.

Just as we created a specific layout resource file for landscape orientation, we can create a layout resource file targeting a particular screen size group. We associate the resource file with the desired screen size group by placing the file in the appropriately named resource folder. For example, we place a layout resource file designed for devices with a screen size group of large in the layout-large resource folder.

Screen size groups date back to the early days of Android when there was little practical experience in dealing with the wide variety of device form factors that now exist. As time has gone on and the number of Android device form factors has grown, file size groups have turned out to be a less than ideal solution. Problems with screen size groups stem from two main issues:

  1. The size range of the groups is not consistently applied, which results in the size range within the groups overlapping one another. One device with a 7-inch screen might be classified as large while another device with the same size screen might be classified as xlarge.
  2. Groups are sometimes too broad. For example, the large group includes devices with 5-inch screens and devices with 7-inch screens. These screen sizes tend to have very different layout requirements. A device with a 5-inch screen tends to work best with handset-style layouts like that for a smartphone, whereas a device with a 7-inch screen tends to work best with a tablet-style layout.

Size groups are still in use because they are the best option available for dealing with screen size differences on pre-API Level 13 devices. Fortunately, less than half of the Android devices in use today are pre-API Level 13, and the ratio is shrinking rapidly.

At API Level 13 (Android 3.2), Android introduced a substantial improvement over screen size groups known as resource screen size qualifiers. Resource screen size qualifiers allow us to associate resources with specific screen size requirements. With screen size qualifiers, we have a very detailed level of control over which layout resources are associated with each device form factor.

To avoid the complications inherent in the wide variety of screen pixel densities and physical screen sizes available, Android uses a canonicalized unit of measure called the density independent pixel (dp) when managing screen sizes. If you've been working with Android for any length of time, you are probably already familiar with density independent pixels as they are the preferred unit of measure when positioning and sizing views within an Android user interface.

A dp always corresponds to the physical size of a pixel on a 160 dpi device and therefore provides a consistent unit of measure independent of the physical pixel size of the device. For example, one 7-inch display device may have a physical pixel count of 1280x720 while another 7-inch display device has a physical pixel count of 1920x1080, but both devices have a dp count of approximately 1000x600. The Android platform takes care of the details of mapping between density independent pixels and the physical pixels of a device.

Android provides three types of screen size qualifiers: smallest width, available screen width, and available screen height:

As the number of form factors our application targets grow, managing the resource files within the different layout resource folders can become somewhat complicated due to the fact that we'll likely want to use the same layout resource file for different qualifiers. To demonstrate this problem, let's update our application to use the version of the activity_main.xml resource file we currently use for landscape-oriented devices on other devices. We'll use that same resource file on devices in the large screen size group and on devices with a current width of 600 dp or greater.

We first create two additional folders under our res folder: layout-large and layout-w600dp. We then copy the activity_main.xml file from the layout-land folder to the two folders we just created. Doing this is easy enough but we now have a maintenance headache. Every time we make a change to that layout, we have to be sure that we make it in all three folders.

To avoid this resource file duplication, we can use layout aliasing.

Layout aliasing allows us to have just a single copy of each layout resource file. We can then provide the resource system with information as to which file to choose for each form factor.

To get started, we'll rename the activity_main.xml resource file in the layout-land resource folder as activity_main_wide.xml. We then move the file to the layout resource folder and delete the layout-land folder.

We now create a new resource folder called values-land under the res folder. To create the folder in Android Studio, follow the same steps as we used earlier to create the layout-land folder except set the Resource type: as values rather than layout.

Within this folder, we create a new resource file, the name of which doesn't matter, but the file containing values for aliasing is often named refs.xml because it contains a list of references to other resources, so that's what we'll use. To create the file using Android Studio, perform the following steps:

In the refs.xml file, be sure that there is already a root element named resources. Within that element, add an item element with a type attribute value of layout. This indicates that we're providing an alias entry for a layout resource. We set the value of the name attribute to be the name of the default layout resource, which in our case is activity_main. We then give the item element a value of @layout/activity_main_wide. The complete refs.xml resource file now appears as follows:

With this file in place, any call to load the layout resource R.layout.activity_main will instead load R.layout.activity_main_wide when the application is running on a device in landscape orientation.

To add support for devices in the large group and those with a current width of at least 600 dp, we simply create two more resource folders, values-large and values-w600dp, and copy the refs.xml file from the values-land folder to each. The layout and values resource folders now appear as shown in the following screenshot:

Layout aliasing

We now have support for all the desired form factors with no unnecessary duplication of layout resource files. We do have duplication of the refs.xml file, but it's a much simpler file than the layout resource file and is less likely to change.

With our user interface well-partitioned and adaptable, we need to be sure that each fragment functions effectively, as layout differences cause the behavior and possibly even the existence of other fragments within the activity to change. When an application user interface is divided into fragments, the fragments exist completely independent of one another rarely. Very often a user's interaction with one fragment has some effect on other fragments within the same activity. In the case of our application, this issue arises when a user selects a book within BookListFragment. In response to the user's selection, the application is responsible for displaying the corresponding description in BookDescFragment.

Instead of creating direct relationships between the fragments, we can take advantage of the abstraction provided by interfaces. By defining a simple callback interface to represent the act of a user making a book selection, we can completely eliminate tight coupling between fragments. The BookListFragment class can be written to provide notification of a user selection through the interface. By implementing the interface on the activity, the activity can then handle coordinating the user selection within BookListFragment with updating the displayed description within BookDeskFragment.

The callback interface should include methods for any interaction with the fragment that may be meaningful to the activity containing the fragment. At the same time, the interface should not burden the activity with unnecessary details. The interface should be focused on application-level actions such as selecting a book rather than implementation-level actions such as tapping on a radio button. The implementation-level details should be isolated within the fragment. We should also be sure to design the interface without any preconceived ideas of what the activity will do with the notification.

In the case of BookListFragment, the only action of interest to the activity is the user selecting a book. This tells us the interface needs just a single method; we'll call the interface method onSelectedBookChanged. We know in the case of this application, the goal is to display the selected book description, so one possibility is to have the onSelectedBookChanged method include a parameter for the book description. The problem with passing the book description is that doing so limits the use of BookListFragment to just this one use case, displaying the book description. Instead, by passing an identifier for the book, BookListFragment is available for any use case in which the user selects a book. For simplicity, in our example, we'll use an array index as the identifier; in a real scenario, the identifier would more likely be a key to locate the book information within a data store or service.

We'll call our new interface OnSelectedBookChangeListener. The interface looks like this:

The BookListFragment class needs to hide the details of user selections and instead translate each selection to a book identifier, which in our case is an array index. We first need to update the BookListFragment class to handle the radio button selections by implementing the RadioGroup.OnCheckedChangeListener interface as follows:

Within the BookListFragment class' onCreateView method, we set the radio group's click listener as the BookListFragment class as shown here:

There are a number of ways to determine the book index corresponding to the selected radio button such as setting the tag value on each radio button or using a lookup table. For simplicity, we'll create a simple method containing a switch statement like the following code:

Within the BookDescFragment class, we want to encapsulate any details about how the user interface is updated. We'll do this by providing a simple method that accepts the book index and handles the details of locating and displaying the book description. Before we can implement that method, we first need to update the BookDescFragment class' onCreateView method to retrieve the list of book descriptions, retrieve a reference to TextView identified by R.id.bookDescription, and assign both to class-level fields as shown here:

We can now add a setBook method that accepts the book index, accesses the appropriate book description, and updates mBookDescriptionTextView. The setBook method appears as follows:

Good use of interfaces and encapsulation greatly simplify using any component, and fragments are no different. With the work we've done on the BookListFragment and BookDescFragment classes, our activity can now coordinate user interaction in BookListFragment by updating BookDescFragment in three simple steps:

Have a look at step 2 first. Unlike when working with views, an activity cannot directly reference the fragments contained within it. Instead, fragment handling is delegated to the FragmentManager class.

Each activity has a unique instance of the FragmentManager class. The FragmentManager class handles access to and management of all fragments within that activity. An activity accesses its FragmentManager instance with the getFragmentManager method.

With FragmentManager, an activity can access the contained fragments by calling the FragmentManager.findFragmentById method and passing the desired fragment's id value from the layout resource.

By using FragmentManager to access BookDescFragment, we can now implement the BookListFragment.OnSelectedBookChangeListener interface on our activity to update the displayed description for each user selection in BookListFragment.

public class MainActivity extends Activity
    implements OnSelectedBookChangeListener{

  @Override
  public void onSelectedBookChanged(int bookIndex) {
    // Access the FragmentManager
    FragmentManager fragmentManager = getFragmentManager();
    // Get the book description fragment
    BookDescFragment bookDescFragment = (BookDescFragment)   
        fragmentManager.findFragmentById(R.id.fragmentDescription);
    // Display the book title
    if(bookDescFragment != null)
      bookDescFragment.setBook(bookIndex);
  }

  // other members elided for clarity
}