16 min read

In this article by Jim Wilson, the author of the book, Creating Dynamic UI with Android Fragments – Second Edition, we will see how to create an adaptive application layout.

(For more resources related to this topic, see here.)

In our application, we’ll leave the wide-display aspect of the program alone because static layout management is working fine there. We work on the portrait-oriented handset aspect of the application. For these devices, we’ll update the application’s main activity to dynamically switch between displaying the fragment containing the list of books and the fragment displaying the selected book’s description.

Updating the layout to support dynamic fragments

Before we write any code to dynamically manage the fragments within our application, we first need to modify the activity layout resource for portrait-oriented handset devices. This resource is contained in the activity_main.xml layout resource file that is not followed by (land) or (600dp). The layout resource currently appears as shown here:

<LinearLayout

android_orientation="vertical"
android_layout_width="match_parent"
android_layout_height="match_parent"
>
<!-- List of Book Titles -->
<fragment
   android_layout_width="match_parent"
   android_layout_height="0dp"
   android_layout_weight="1"
   android_name="com.jwhh.fragments.BookListFragment2"
   android_id="@+id/fragmentTitles"
   tools_layout="@layout/fragment_book_list"/>
</LinearLayout>

We need to make two changes to the layout resource. The first is to add an id attribute to the LinearLayout view group so that we can easily locate it in code. The other change is to completely remove the fragment element. The updated layout resource now contains only the LinearLayout view group, which includes an id attribute value of @+id/layoutRoot. The layout resource now appears as shown here:

<LinearLayout

android_id="@+id/layoutRoot"
android_orientation="vertical"
android_layout_width="match_parent"
android_layout_height="match_parent"
>
</LinearLayout>

We still want our application to initially display the book list fragment, so removing the fragment element may seem like a strange change, but doing so is essential as we move our application to dynamically manage the fragments. We will eventually need to remove the book list fragment to replace it with the book description fragment. If we were to leave the book list fragment in the layout resource, our attempt to dynamically remove it later would silently fail.

Only dynamically added fragments can be dynamically removed. Attempting to dynamically remove a fragment that was statically added with the fragment element in a layout resource will silently fail.

Adapting to device differences

When our application is running on a portrait-oriented handset device, the activity needs to programmatically load the fragment containing the book list. This is the same Fragment class, BookListFragment2, we were previously loading with the fragment element in the activity_main.xml layout resource file. Before we load the book list fragment, we first need to determine whether we’re running on a device that requires dynamic fragment management. Remember that, for the wide-display devices, we’re going to leave the static fragment management in place.

There’ll be a couple of places in our code where we’ll need to take different logic paths depending on which layout we’re using. So we’ll need to add a boolean class-level field to the MainActivity class in which we can store whether we’re using dynamic or static fragment management:

boolean mIsDynamic;

We can interrogate the device for its specific characteristics such as screen size and orientation. However, remember that much of our previous work was to configure our application to take the advantage of the Android resource system to automatically load the appropriate layout resources based on the device characteristics. Rather than repeating those characteristics checks in code, we can instead simply include the code to determine which layout resource was loaded. The layout resource for wide-display devices we created earlier, activity_main_wide.xml, statically loads both the book list fragment and the book description fragment. We can include in our activity’s onCreate method code to determine whether the loaded layout resource includes one of those fragments as shown here:

public class MainActivity extends Activity
implements BookListFragment.OnSelectedBookChangeListener {
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main_dynamic);

   // Get the book description fragment
   FragmentManager fm = getFragmentManager();
   Fragment bookDescFragment =
     fm.findFragmentById(R.id.fragmentDescription);

   // If not found than we're doing dynamic mgmt
   mIsDynamic = bookDescFragment == null ||
     !bookDescFragment.isInLayout();
}

// Other members elided for clarity
}

When the call to the setContentView method returns, we know that the appropriate layout resource for the current device has been loaded. We then use the FragmentManager instance to search for the fragment with an id value of R.id.fragmentDescription that is included in the layout resource for wide-display devices, but not the layout resource for portrait-oriented handsets. A return value of null indicates that the fragment was not loaded and we are, therefore, on a device that requires us to dynamically manage the fragments. In addition to the test for null, we also include the call to the isInLayout method to protect against one special case scenario.

In the scenario where the device is in a landscape layout and then rotated to portrait, a cached instance to the fragment identified by R.id.fragmentDescription may still exist even though in the current orientation, the activity is not using the fragment. By calling the isInLayout method, we’re able to determine whether the returned reference is part of the currently loaded layout. With this, our test to set the mIsDynamic member variable effectively says that we’ll set mIsDynamic to true when the R.id.fragmentDescription fragment is not found (equals null), or it’s found but is not a part of the currently loaded layout (!bookDescFragment.isInLayout).

Dynamically loading a fragment at startup

Now that we’re able to determine whether or not dynamically loading the book list fragment is necessary, we add the code to do so to our onCreate method as shown here:

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_dynamic);

// Get the book description fragment
FragmentManager fm = getFragmentManager();
Fragment bookDescFragment =
   fm.findFragmentById(R.id.fragmentDescription);

// If not found than we're doing dynamic mgmt
mIsDynamic = bookDescFragment == null ||
   !bookDescFragment.isInLayout();

// Load the list fragment if necessary
if (mIsDynamic) {
   // Begin transaction
   FragmentTransaction ft = fm.beginTransaction();

   // Create the Fragment and add
   BookListFragment2 listFragment = new BookListFragment2();
   ft.add(R.id.layoutRoot, listFragment, "bookList");

   // Commit the changes
   ft.commit();
}
}

Following the check to determine whether we’re on a device that requires dynamic fragment management, we include FragmentTransaction to add an instance of the BookListFragment2 class to the activity as a child of the LinearLayout view group identified by the id value R.id.layoutRoot. This code capitalizes on the changes we made to the activity_main.xml resource file by removing the fragment element and including an id value on the LinearLayout view group.

Now that we’re dynamically loading the book list, we’re ready to get rid of that other activity.

Transitioning between fragments

As you’ll recall, whenever the user selects a book title within the BookListFragment2 class, the fragment notifies the main activity by calling the MainActivity.onSelectedBookChanged method passing the index of the selected book. The onSelectedBookChanged method currently appears as follows:

public void onSelectedBookChanged(int bookIndex) {
FragmentManager fm = getFragmentManager();
// Get the book description fragment
BookDescFragment bookDescFragment = (BookDescFragment)
     fm.findFragmentById(R.id.fragmentDescription);

// Check validity of fragment reference
if(bookDescFragment == null || !bookDescFragment.isVisible()){
   // Use activity to display description
   Intent intent = new Intent(this, BookDescActivity.class);
   intent.putExtra("bookIndex", bookIndex);
   startActivity(intent);
}
else {
   // Use contained fragment to display description
   bookDescFragment.setBook(bookIndex);
}
}

In the current implementation, we use a technique similar to what we did in the onCreate method to determine which layout is loaded; we try to find the book description fragment within the currently loaded layout. If we find it, we know the current layout includes the fragment, so we go ahead and set the book description directly on the fragment. If we don’t find it, we call the startActivity method to display the activity that contains the book description fragment.

Starting a separate activity to handle the interaction with the BookListFragment2 class unnecessarily adds complexity to our program. Doing so requires that we pass data from one activity to another, which can sometimes be complex, especially if there are a large number of values or some of those values are object types that require additional coding to be passed in an Intent instance. More importantly, using a separate activity to manage the interaction with the BookListFragment2 class results in redundant work due to the fact that we already have all the code necessary to interact with the BookListFragment2 class in the MainActivity class. We’d prefer to handle the interaction with the BookListFragment2 class consistently in all cases.

Eliminating redundant handling

To eliminate this redundant handling, we start by stripping any code in the current implementation that deals with starting an activity. We can also avoid repeating the check for the book description fragment because we performed that check earlier in the onCreate method. Instead, we can now check the mIsDynamic class-level field to determine the proper handling. With that in mind, now we can initially modify the onSelectedBookChanged method to look like the following code:

public void onSelectedBookChanged(int bookIndex) {
BookDescFragment bookDescFragment;
FragmentManager fm = getFragmentManager();

// Check validity of fragment reference
if(mIsDynamic) {
   // Handle dynamic switch to description fragment
}
else {
   // Use the already visible description fragment
   bookDescFragment = (BookDescFragment)
   fm.findFragmentById(R.id.fragmentDescription);
   bookDescFragment.setBook(bookIndex);
}
}

We now check the mIsDynamic member field to determine the appropriate code path. We still have some work to do if it turns out to be true, but in the case of it being false, we can simply get a reference to the book description fragment that we know is contained within the current layout and set the book index on it much like we were doing before.

Creating the fragment on the fly

In the case of the mIsDynamic field being true, we can display the book description fragment by simply replacing the book list fragment we added in the onCreate method with the book description fragment using the code shown here:

FragmentTransaction ft = fm.beginTransaction();
bookDescFragment = new BookDescFragment();
ft.replace(R.id.layoutRoot, bookDescFragment, "bookDescription");
ft.addToBackStack(null);
ft.setCustomAnimations(
android.R.animator.fade_in, android.R.animator.fade_out);
ft.commit();

Within FragmentTransaction, we create an instance of the BookDescFragment class and call the replace method passing the ID of the same view group that contains the BookListFragment2 instance that we added in the onCreate method. We include a call to the addToBackStack method so that the back button functions correctly, allowing the user to tap the back button to return to the book list.

The code includes a call to the FragmentTransaction class’ setCustomAnimations method that creates a fade effect when the user switches from one fragment to another.

Managing asynchronous creation

We have one final challenge that is to set the book index on the dynamically added book description fragment. Our initial thought might be to simply call the BookDescFragment class’ setBook method after we create the BookDescFragment instance, but let’s first take a look at the current implementation of the setBook method. The method currently appears as follows:

public void setBook(int bookIndex) {
// Lookup the book description
String bookDescription = mBookDescriptions[bookIndex];

// Display it
mBookDescriptionTextView.setText(bookDescription);
}

The last line of the method attempts to set the value of mBookDescriptionTextView within the fragment, which is a problem. Remember that the work we do within a FragmentTransaction class is not immediately applied to the user interface. Instead, as we discussed earlier in this chapter, in the deferred execution of transaction changes section, the work within the transaction is performed sometime after the completion of the call to the commit method. Therefore, the BookDescFragment instance’s onCreate and onCreateView methods have not yet been called. As a result, any views associated with the BookDescFragment instance have not yet been created. An attempt to call the setText method on the BookDescriptionTextView instance will result in a null reference exception.

One possible solution is to modify the setBook method to be aware of the current state of the fragment. In this scenario, the setBook method checks whether the BookDescFragment instance has been fully created. If not, it will store the book index value in the class-level field and later automatically set the BookDescriptionTextView value as part of the creation process. Although there may be some scenarios that warrant such a complicated solution, fragments give us an easier one.

The Fragment base class includes a method called setArguments. With the setArguments method, we can attach data values, otherwise known as arguments, to the fragment that can then be accessed later in the fragment lifecycle using the getArguments method. Much like we do when associating extras with an Intent instance, a good practice is to define constants on the target class to name the argument values. It is also a good programming practice to provide a constant for an argument default value in the case of non-nullable types such as integers as shown here:

public class BookDescFragment extends Fragment {
// Book index argument name
public static final String BOOK_INDEX = "book index";

// Book index default value
private static final int BOOK_INDEX_NOT_SET = -1;

// Other members elided for clarity
}

If you used Android Studio to generate the BookDescFragment class, you’ll find that the ARG_PARAM1 and ARG_PARAM2 constants are included in the class. Android Studio includes these constants to provide examples of how to pass values to fragments just as we’re discussing now. Since we’re adding our own constant declarations, you can delete the ARG_PARAM1 and ARG_PARAM2 constants from the BookDescFragment class and also the lines in the generated BookDescFragment.onCreate and BookDescFragment.newInstance methods that reference them.

We’ll use the BOOK_INDEX constant to get and set the book index value and the BOOK_INDEX_NOT_SET constant to indicate whether the book index argument has been set.

To simplify the process of creating the BookDescFragment instance and passing it the book index value, we’ll add a static factory method named newInstance to the BookDescFragment class that appears as follows:

public static BookDescFragment newInstance(int bookIndex) {
BookDescFragment fragment = new BookDescFragment();
Bundle args = new Bundle();
args.putInt(BOOK_INDEX, bookIndex);
fragment.setArguments(args);
return fragment;
}

The newInstance methods start by creating an instance of the BookDescFragment class. It then creates an instance of the Bundle class, stores the book index in the Bundle instance, and then uses the setArguments method to attach it to the BookDescFragment instance. Finally, the newInstance method returns the BookDescFragment instance. We’ll use this method shortly within the MainActivity class to create our BookDescFragment instance.

If you used Android Studio to generate the BookDescFragment class, you’ll find that most of the newInstance method is already in place. The only change you’ll have to make is replace the two lines that referenced the ARG_PARAM1 and ARG_PARAM2 constants you deleted with the call to the args.putInt method shown in the preceding code.

We can now update the BookDescFragment class’ onCreateView method to look for arguments that might be attached to the fragment. Before we make any changes to the onCreateView method, let’s look at the current implementation that appears as follows:

public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState)
{
View viewHierarchy = inflater.inflate(
   R.layout.fragment_book_desc, container, false);

// Load array of book descriptions
mBookDescriptions =   getResources().getStringArray(R.array.bookDescriptions);
// Get reference to book description text view
mBookDescriptionTextView = (TextView)
viewHierarchy.findViewById(R.id.bookDescription);

return viewHierarchy;
}

As the onCreateView method is currently implemented, it simply inflates the layout resource, loads the array containing the book descriptions, and caches a reference to the TextView instance where the book description is loaded.

We can now update the method to look for and use a book index that might be attached as an argument. The updated method appears as follows:

public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState)
{
View viewHierarchy = inflater.inflate(
   R.layout.fragment_book_desc, container, false);

// Load array of book descriptions
mBookDescriptions =
   getResources().getStringArray(R.array.bookDescriptions);
// Get reference to book description text view
mBookDescriptionTextView = (TextView)
viewHierarchy.findViewById(R.id.bookDescription);

// Retrieve the book index if attached
Bundle args = getArguments();
int bookIndex = args != null ?
args.getInt(BOOK_INDEX, BOOK_INDEX_NOT_SET) :
BOOK_INDEX_NOT_SET;

// If we find the book index, use it
if (bookIndex != BOOK_INDEX_NOT_SET)
   setBook(bookIndex);

return viewHierarchy;
}

Just before we return the fragment’s view hierarchy, we call the getArguments method to retrieve any arguments that might be attached. The arguments are returned as an instance of the Bundle class. If the Bundle instance is non-null, we call the Bundle class’ getInt method to retrieve the book index and assign it to the bookIndex local variable. The second parameter of the getInt method, BOOK_INDEX_NOT_SET, is returned if the fragment happens to have arguments attached that do not include the book index. Although this should not normally be the case, being prepared for any such an unexpected circumstance is a good idea. Finally, we check the value of the bookIndex variable. If it contains a book index, we call the fragment’s setBook method to display it.

Putting it all together

With the BookDescFragment class now including support for attaching the book index as an argument, we’re ready to fully implement the main activity’s onSelectedBookChanged method to include switching to the BookDescFragment instance and attaching the book index as an argument. The method now appears as follows:

public void onSelectedBookChanged(int bookIndex) {
BookDescFragment bookDescFragment;
FragmentManager fm = getFragmentManager();

// Check validity of fragment reference
if(mIsDynamic){
   // Handle dynamic switch to description fragment
   FragmentTransaction ft = fm.beginTransaction();

   // Create the fragment and pass the book index
   bookDescFragment = BookDescFragment.newInstance(bookIndex);

   // Replace the book list with the description
   ft.replace(R.id.layoutRoot,
     bookDescFragment, "bookDescription");
   ft.addToBackStack(null);
   ft.setCustomAnimations(
     android.R.animator.fade_in, android.R.animator.fade_out);
   ft.commit();
}
else {
   // Use the already visible description fragment
   bookDescFragment = (BookDescFragment)
     fm.findFragmentById(R.id.fragmentDescription);
   bookDescFragment.setBook(bookIndex);
}
}

Just as before, we start with the check to see whether we’re doing dynamic fragment management. Once we determine that we are, we start the FragmentTransaction instance and create the BookDescFragment instance. We then create a new Bundle instance, store the book index into it, and then attach the Bundle instance to the BookDescFragment instance with the setArguments method. Finally, we put the BookDescFragment instance into place as the current fragment, take care of the back stack, enable animation, and complete the transaction.

Everything is now complete. When the user selects a book title from the list, the onSelectedBookChanged method gets called. The onSelectedBookChanged method then creates and displays the BookDescFragment instance with the appropriate book index attached as an argument. When the BookDescFragment instance is ultimately created, its onCreateView method will then retrieve the book index from the arguments and display the appropriate description.

Summary

In this article, we saw that using the FragmentTransaction class, we’re able to dynamically switch between individual fragments within an activity, eliminating the need to create a separate activity class for each screen in our application. This helps to prevent the proliferation of unnecessary activity classes, better organize our applications, and avoid the associated increase in complexity.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here