Lecture 6 Fragments

This lecture discusses Android Fragments. A Fragment is “a behavior or a portion of user interface in Activity.” You can think of them as “mini-activities” or “sub-activities”. Fragments are designed to be reusable and composable, so you can mix and match them within a single screen of a user interface. While XML resource provide reusable and composable views, Fragments provide reusable and composable controllers. Fragments allow us to make re-usable pieces of Activities that can have their own layouts, data models, event callbacks, etc.

This lecture references code found at https://github.com/info448/lecture06-fragments.

Fragments were introduced in API 11 (Honeycomb), which provided the first “tablet” version of Android. Fragments were designed to provide a UI component that would allow for side-by-side activity displays appropriate to larger screens:

Fragment example, from Google.

Fragment example, from Google17.

Instead of needing to navigate between two related views (particularly for this “master and detail” setup), the user can see both views within the same Activity… but those “views” could also be easily split between two Activities for smaller screens, because their required controller logic has been isolated into a Fragment.

Fragments are intended to be modular, reusable components. They should not depend on the Activity they are inside, so that you can be flexible about when and where they are displayed!

Although Fragments are like “mini-Activities”, they are always embedded inside an Activity; they cannot exist independently. While it’s possible to have Fragments that are not visible or that don’t have a UI, they still are part of an Activity. Because of this, a Fragment’s lifecycle is directly tied to its containing Activity’s lifecycle. (e.g., if the Activity is paused, the Fragment is too. If the Activity is destroyed, the Fragment is too). However, Fragments also have their own lifecycle with corresponding lifecycle callbacks functions.

Fragment lifecycle state diagram, from Google.

Fragment lifecycle state diagram,
from Google18.

The Fragment lifecycle is very similar to the Activity lifecycle, with a couple of additional steps:

  • onAttach(): called when the Fragment is first associated with (“added to”) an Activity, and thus gains a Context. This callback is generally used for initializing communication between the Fragment and its Activity.

    This callback is mirrored by onDetach(), for when the Fragment is removed from an Activity.

  • onCreateView(): called when the View (the user interface) is about to be drawn. This callback is used to establish any details dependent on the View (including adding event listeners, etc).

    Note that code initializing data models, or anything that needs to be persisted across configuration changes, should instead be done in the onCreate() callback. onCreate() is not called if the fragment is retained (see below).

    This callback is mirrored by onDestroyView(), for when the Fragment’s UI View hierarchy is being removed from the screen.

  • onActivityCreated(): called when the containing Activity’s onCreate() method has returned, and thus indicates that the Activity is fully created. This is useful for retained Fragments.

    This callback has no mirror!

6.1 Creating a Fragment

In order to illustrate how to make a Fragment, we will refactor the MainActivity to use Fragments for displaying a list of movies. This will help to illustrate the relationship between Activities and Fragments.

To create a Fragment, you subclass the Fragment class. Let’s make one called MovieListFragment (in a MovieListFragment.java file). You can use Android Studio to do this work: via the File > New > Fragment > Fragment (blank) menu option. (DO NOT select any of the other options for in the wizard for now; they provide template code that can distract from the core principles).

There are two versions of the Fragment class: one in the framework’s android.app package and one in the android.support.v4 package. The later package refers to the Support Library. As discussed in the previous chapter, these are libraries of classes designed to make Android applications backwards compatible: for example, Fragment and its related classes came out in API 11 so aren’t in the android.app package for earlier devices. By including the support library, we can include those classes on older devices as well!

  • Support libraries also include additional convenience and helper classes that are not part of the core Android package. These include interface elements (e.g., ConstraintLayout, RecyclerView, or ViewPager) and accessibility classes. See the features list for details. Thus it is often useful to include and utilize support library versions of classes even when targeting later devices so that you don’t need to “roll your own” versions of these convenience classes.

  • The main disadvantage to using support libraries is that they need to be included in your application, so will make the final .apk file larger (and may potentially require workarounds for method count limitations). You will also run into problems if you try and mix and match versions of the classes (e.g., from different versions of the support library). But as always, you should avoid premature optimization. Thus in this course you should default to using the support library version of a class when given a choice!

  • Note that the androidx package that is being developed as part of Jetpack will likely be replacing support libraries.

After we’ve created the MovieListFragment.java file, we’ll want to specify a layout for that Fragment (so it can be shown on the screen). As part of using the New Fragment Wizard we were provided with a fragment_movie_list layout that we can use.

  • Since we want the Movie list to live in that Fragment, we can move (copy) the View definitions from activity_main into fragment_movie_list.
  • We will then adjust activity_main so that it instead contains an empty FrameLayout. This will act as a simple “container” for our Fragment (similar to an empty <div> in HTML). Be sure to give it an id so we can refer to it later!.

It is possible to include the Fragment directly through the XML, using the XML to instantiate the Fragment (the same way that we have the XML instantiate Buttons). We do this by specifying a <fragment> element, with an android:name attribute assigned a reference to the Fragment class:

<fragment
   android:id="@+id/fragment"
     android:name="edu.uw.fragmentdemo.MovieListFragment"
     android:layout_width="match_parent"
     android:layout_height="match_parent"/>

Defining the Fragment in the XML works (and will be fine to start with), but in practice it is much more common and worthwhile to instantiate the Fragments dynamically at runtime in the Java/Kotlin code—thereby allowing the Fragments to be dynamically determined and changed. We will start with the XML version to build the Fragment initially, and then shift to the Kotlin version to talk about changing Fragments.

We can next begin filling in the Kotlin logic for the Fragment. Android Studio provides a little starter code: the onCreateView() callback, which is what we will use that to set up the layout (similar to what we do with the onCreate() method of MainActivity). But the MainActivity#onCreate() method specifies a layout by calling setContentView() and passing a resource id. With Fragments, we can’t just “set” the View because the Fragment belongs to an Activity, and so will exist inside of that Activity’s View hierarchy! Instead, we need to figure out which ViewGroup the Fragment is inside of, and then inflate the Fragment’s View inside that ViewGroup.

This “inflated” View is referred to as the root view: it is the “root” of the Fragment’s View tree (the View that all the Views inside the Fragment’s layout will be attached to). We access the root view by inflating the fragment’s layout, and saving a reference to the inflated View:

//java
View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
//kotlin
val rootView = inflater.inflate(R.layout.fragment_layout, container, false)
  • Note that the inflater object we are calling inflate() on was passed as a parameter to the onCreateView() callback. The parameters to the inflate() method are: the layout to inflate, the ViewGroup (container) into which the layout should be inflated (also passed as a parameter to the callback), and whether or not to “attach” the inflated layout to the container (false in this case because the Fragment system already handles the attachment, so the inflate method doesn’t need to). The onCreateView() callback must return the inflated root view, so that the system can perform this attachment.

With the Fragment’s layout defined, we can start moving functionality from the Activity into the Fragment.

  • The ListView and adapter setup will need to be moved over. The UI setup (including initializing the Adapter) will be moved from the Activity’s onCreate() to the Fragment’s onCreateView(). However, you will need to make a few changes during this refactoring:

    • The findViewById() method is a method of the Activity class, and thus can’t be called on an implicit this inside the Fragment. Instead, the method can be called on the root view, searching just that View and its children.

    • The Adapter’s constructor requires a Context as its first parameter; while an Activity is a Context, a Fragment is not—Fragments operate in the Context of their containing Activity! Fragments can refer to the Activity that they are inside (and the Context it represents) by using the getActivity() method (or just the activity property in Kotlin). Note that this method should be used only for getting a reference to a Context, not for arbitrary communication with the Activity (see below for details).

      In Kotlin, because syntactically the Activity is allowed to be null (even though that would never happen), we either have to explicitly note that the Activity is not null with !!activity. Or more idiomatically, we can wrap the Activity-required functionality in a .let() lambda:

      //kotlin
      //if Activity is not null, perform the following on `it`
      activity?.let {
          //do stuff on Activity
          adapter = adapter = ArrayAdapter(it, R.layout.list_item, R.id.txt_item, ArrayList())
      }
  • The downloadMovieData() helper method can be moved over, so that it belongs to the Fragment instead of the Activity. It will be the Fragment’s responsibility to handle data downloading.

Activity-to-Fragment Communication

This example has intentionally left the input controls (the search field and button) in the Activity, rather than making them part of the Fragment. Apart from being a useful demonstration, this allows the Fragment to have a single purpose (showing the list of movies) and would let us change the search UI independent of the displayed results. But since the the button is in the Activity while the downloading functionality is in the Fragment, we need a way for the Activity to “talk” to the Fragment. We thus need a reference to the contained Fragment—access to the XML similar to that provided by findViewById.

We can get a reference to a contained Fragment from an Activity by using a FragmentManager. This is an object responsible for (ahem) managing Fragments. It allows us to “look up” Fragments, as well as to manipulate which Fragments are shown. We access this FragmentManager by calling the getSupportFragmentManager() method (or using the supportFragmentManager property in Kotlin) of the Activity, and then can use findFragmentById() to look up an XML-defined Fragment by its id:

//java
//MovieListFragment example
MovieListFragment fragment = (MovieListFragment)getSupportFragmentManager().findFragmentById(R.id.fragment);
//kotlin
val fragment: MovieListFragment = supportFragmentManager.findFragmentById(R.id.fragment) as MovieListFragment
  • Note that we’re using a method to explicit access the support FragmentManager. The Activity class (API level 15+) is able to work with both the platform and support FragmentManager classes. But because these classes don’t have a shared interface, the Activity needs to provide different Java/Kotlin methods which can return the correct type.

Once you have a reference to the Fragment, this acts just like any other object—you can call any public methods it has! For example, if you give the Fragment a public method (e.g., searchMovies()), then this method can be called from the Activity:

//called from Activity on the referenced fragment
fragment.searchMovies(searchTerm)

(The parameter to this public method allows the Activity to provide information to the Fragment!)

At this point, the program should be able to be executed… and continue to function in exactly the same way! The program has just been refactored, so that all the movie downloading and listing work is encapsulated inside a Fragment that can be used in different Activities.

  • In effect, we’ve created our own “widget” that can be included in any other screen, such as if we repeatedly wanted the list of movies to be available alongside some other user interface components.

6.2 Dynamic Fragments

The real benefit from encapsulating behavior in a Fragment is to be able to support multiple Fragments within a single Activity. For example, in the the archetypal “master/detail” navigation flow, one screen (Fragment) holds the “master list” and another screen (Fragment) holds details about a particular item. This is a very common navigation pattern for Android apps, and can be seen in most email or news apps.

  • On large screens, Fragments allow these two Views to be placed side by side!

In this section, we will continue to refine the Movie app so that when the user clicks on a Movie in the list, the app shows a screen (Fragment) with details about the selected movie.

Instantiating Fragments

To do this, we will need to instantiate the desired Fragment dynamically (in Java code), rather than statically in the XML using the <fragment> element. This is because we need to be able to dynamically change which Fragment is currently being shown, which is not possibly for Fragments that are “hard-coded” in the XML.

Unlike Activities, Fragments (such as MovieListFragment) do have constructor methods that can be called. In fact, Android requires that every Fragment have a default (no-argument) primary constructor that is called when Fragments are created by the system! While we have access to the constructor, it is considered best practice to not call this constructor directly from the Activity when you want to instantiate a Fragment, and to in fact leave the method empty. This is because we do not have full control over when the constructor is executed: the Android system may call the no-argument constructor whenever it needs to recreate the Activity (or just the Fragment), which can happen at arbitrary times. Since only this default constructor is called, we can’t add an additional constructor with any arguments we may want the Fragment to have (e.g., the searchTerm)… and thus it’s best to not use it at all.

Instead, we specify a simple factory method (by convention called newInstance()) which is able to “create” an instance of the Fragment for us. This factory method can take as many arguments as we want, and then does the work of passing these arguments into the Fragment instantiated with the default constructor. Note that this factory needs to be a static method (it is called on the class, not the object), and so is defined in a companion object in Kotlin.

//java
public static MyFragment newInstance(String argument) {
    MyFragment fragment = new MyFragment(); //instantiate the Fragment
    Bundle args = new Bundle(); //an (empty) Bundle for the arguments
    args.putString(ARG_PARAM_KEY, argument); //add the argument to the Bundle
    fragment.setArguments(args); //add the Bundle to the Fragment
    return fragment; //return the Fragment
}
//kotlin
companion object {
    private val ARG_PARAM_KEY = "my key"

    fun newInstance(argument: String): MyFragment {
        val args = Bundle().apply {
            putString(ARG_PARAM_KEY, argument)
        }

        val fragment = MyFragment().apply {
            setArguments(args)
        }
        return fragment
    }
}

In order to pass the arguments into the new Fragment, we wrap them up in a Bundle (an object containing basic key-value pairs). Values can be added to a Bundle using an appropriate putType() method; note that these do need to be primitive types (int, String, etc.). The Bundle of arguments can then be assignment to the Fragment by calling the setArguments() method.

  • In Kotlin, it’s idiomatic to do this work using the apply() method described in the previous chapter. This lets you easily put lots of values in the Bundle without needing to write the word args over and over again.

  • We will be able to access this Bundle from inside the Fragment (e.g., in the onCreateView() callback) by using the getArguments() method, and the getTYPE() methods to retrieve the values from it; in Kotlin you can access these as properties. This allows us to dynamically adjust the content of the Fragment’s Views! For example, we can run the downloadMovieData() function using this argument, fetching movie results as soon as the Fragment is created (e.g., on a button press)… allowing the downloadMovieData() function to again be made private, for example.

  • Since the Bundle is a set of key-value pairs, each value needs to have a particular key. These keys are usually defined as private constants (e.g., ARG_PARAM_KEY in the above example) to make storage and retrieval easier. Remember: hese Bundles are internal to the Fragment!

We will then be able to instantiate the Fragment (e.g., in the Activity class), passing it any arguments we wish:

//java
MyFragment fragment = MyFragment.newInstance("My Argument");
//kotlin
val fragment = MyFragment.newInstance("My Argument")

Transactions

Once we’ve instantiated a Fragment in the Java, we need to attach it to the view hierarchy: since we’re no longer using the XML <fragment> element, we need some other way to load the Fragment into the <FrameLayout> container.

We do this loading using a FragmentTransaction19. A transaction represents a change in the Fragment that is being displayed. You can think of this like a bank (or database) transaction: they allow you to add or remove Fragments like we would add or remove money from a bank account. We instantiate new transactions representing the change we wish to make, and then “run” that transaction in order to apply the change.

To create a transaction, we utilize the FragmentManager again; the FragmentManager#beginTransaction() method is used to instantiate a new FragmentTransaction.

Transactions represent a set of Fragment changes that are all “applied” at the same time (similar to depositing and withdrawing money from multiple bank accounts all at once). We specify these transactions using by calling the add(), remove(), or replace() methods on the FragmentTransaction.

  • The add() method lets you specify which View container you want to add a particular Fragment to. The remove() method lets you remove a Fragment you have a reference to. The replace() method removes any Fragments in the container and then adds the specified Fragment instead.

  • Each of these methods returns the modified FragmentTransaction, so they can be “chained” together. In Kotlin, you can also use the run() method to perform this a block—similar to apply(), but it doesn’t return the object in the end.

Finally, we call the commit() method on the transaction in order to “submit” it and have all of the changes go into effect.

We can do this work in the Activity’s search click handler to add a Fragment, rather than specifying the Fragment in the XML:

//java
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
//params: container to add to, Fragment to add, (optional) tag
transaction.add(R.id.container, myFragment, MOVIE_LIST_FRAGMENT_TAG);
transaction.commit();
//kotlin
supportFragmentManager.beginTransaction().run {
    add(R.id.container, myFragment, MOVIE_LIST_FRAGMENT_TAG)
    commit()
}
  • The third argument for the add() method is a “tag” we apply to the Fragment being added. This gives it a “name” that we can use to find a reference to this Fragment later if we want (via FragmentManager#findFragmentByTag(tag)). Alternatively, we can save a reference to the Fragment as an instance variable; this is faster but more memory intensive (and can cause possible leaks, since the reference keeps the Fragment from being reclaimed by the system).

Inter-Fragment Communication

We can use this transaction-based structure to instantiate and load a second Fragment (e.g., a “detail” view for a selected Movie). We can add functionality (e.g., in the onClick() handler) so that when the user clicks on a movie in the list, we replace() the currently displayed Fragment with this new details Fragment.

However, remember that Fragments are supposed to be modular—each Fragment should be self-contained, and not know about any other Fragments that may exist (after all, what if we wanted the master/detail views to be side-by-side on a large screen, instead of the list replacing itself?)

Using getActivity() to reference the Activity and getSupportFragmentManager() to access the manager is a violation of the Law of Demeter—don’t do it!

Instead, we have Fragments communicate by passing messages through their contained Activity: the MovieFragment should tell its Activity that a particular movie has been selected, and then that Activity can determine what to do about it (e.g., creating a DetailFragment to display that information).

The recommended way to provide Fragment-to-Activity communication is to define an interface. The Fragment class should specify an interface (for one or more public methods) that its containing Activity must support—and since the Fragment can only exist within an Activity that implements that interface, it knows the Activity has the specified public methods that it can call to pass information to that Activity.

As an example of this process:

  • Create a new interface inside the Fragment (e.g., OnMovieSelectedListener). This interface needs a public method (e.g., onMovieSelected(Movie movie)) that the Fragment can call to give instructions or messages to the Activity.

  • In the Fragment’s onAttach() callback (called when the Fragment is first associated with an Activity), we can check that the Activity actually implements the interface by trying to cast it to that interface. We can also save a reference to this Activity for later, to save some time:

    //java
    public void onAttach(Context context) {
        super.onAttach(context);
    
        try {
            callback = (OnMovieSelectedListener)context; //attempt to cast
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement OnMovieSelectedListener");
        }
    }
    //kotlin
    override fun onAttach(context: Context?) {
        super.onAttach(context)
    
        callback = context as? OnMovieSelectedListener
        if(callback == null){
            throw ClassCastException("$context must implement OnMovieSelectedListener")
        }
    }   
  • Then when an action occurs in the Fragment (e.g., a movie is selected), you call the interface’s method on the callback reference.

  • Finally, you will need to make sure that the Activity implements this callback. Remember that a class can implement multiple interfaces!

    In the Activity’s implementation of the interface, you can handle the information provided. For example, use the FragmentManager to create a replace() transaction to load a new DetailFragment for the appropriate data.

In the end, this will allow you to have one Fragment cause the application to switch to another!

This is not the only way for Fragments to communicate. It is also possible to have a Fragment send an Intent to the Activity, who then responds to that as appropriate. But using the Intent system is more resource-intensive than using interfaces.

Parcelable

It’s not uncommon to want to pass multiple or complex data values as arguments to a new Fragment, such as a Movie object. However, the arguments for a new Fragment must be passed in through a Bundle, which are only able to store primitive values (e.g., int, float, String)… and a special data type called Parcelable. An object that implements the Parcelable interface includes methods that allow it to be serialized into a value that is simple enough to place in a Bundle—in effect, it’s values are converted into a formatted String (similar in concept to JSON.stringify() in JavaScript).

Implementing the Parcelable interface involves the following steps: 1. provide a writeToParcel() method that adds the object’s fields to a given Parcel object 2. provide a constructor that can re-create the object from a Parcel by reading the values from it (in the EXACT SAME order they were added!) 3. provide a describeContents() that returns whether the parcel includes a file descriptor (usually 0, meaning not) 4. provide a Parcelable.Creator constant that can be used to create the Parcelable.

These steps seem complex but are fairly rote: as such, it’s possible to “automate” making a class into a Parcelable. My favorite strategy for Java is to use http://www.parcelabler.com/, a website that allows you to copy and paste a class and provides you a version of that class with the Parcelable interface implemented!

In Kotlin it’s even easier: if you include the Kotlin Android Extensions, you can activate experimental mode by adding a declaration to your app’s build.gradle file. Once that has been included, you can simply annotate the class with the @Parcelize annotation, and the clas will automatically be provided with the methods to implement the Parcelable interface.

Bundles are only supposed to store small amounts of information (a few variables); and by extension Parcelable objects should also only include a few attributes. Never make an object that contains an array or a list into a Parcelable! Complex or variable-length data should instead be persisted separate from the Activity (e.g., in a database), with identifiers passed in Bundles instead.

The Back Stack

But what happens when we hit the “back” button? The Activity exits! Why? Because “back” normally says to “leave the Activity”—we only had one Activity, even though there were multiple fragments.

Recall that the Android system may have lots of Activities (even across multiple apps!) with the user moving back and forth between them. As described in Lecture 3, each new Activity is associated with a “task” and placed on a stack20. When the “back” button is pressed, that Activity is popped off the stack, and the user is taken to the Activity that is now at the top.

Fragments by default are not part of this “back-stack”, since they are just components of Activities. However, you can specify that a transaction should include the Fragment change as part of the stack navigation by calling FragmentTransaction#addToBackStack() as part of your transaction (e.g., right before you commit()):

//java
getSupportFragmentManager().beginTransaction()
                           .add(detailFragment, "detail")
                           // Add this transaction to the back stack
                           .addToBackStack(null)
                           .commit();

Note that the “back” button will cause the entire transaction to “reverse”. Thus if you performed a remove() then an add() (e.g., via a replace()), then hitting “back” will cause the the previously added Fragment to be removed and the previously removed Fragment to be added.

  • FragmentManager also includes numerous methods for manually manipulating the back-stack (e.g., “popping” off transactions) if you want to include custom navigation elements such as extra “back” buttons.