Working with Fragments on Android - Part 2

After a long delay, here is the second part in the Working with Fragments series. If you haven't read the first part then you can do so here.

In this part of the tutorial I am going to cover how to implement a simple tab interface using only Fragments. If you have ever messed around with Android's TabWidget and TabHost APIs, then you probably know what a nightmare it can be to deal with tab interfaces on Android. The good news is that it is very easy to reproduce that interface using Fragments and by doing so it gives you far more control over the look and feel of your tabs.

Tabs

Just like TabHost and TabWidget, a Fragment tab interface can be broken into two parts: the tab content, and the tab controller. The tab controller will change the tab content when the user selects one of the tabs. The app I built contains a Fragment called TabFragment that acts as the controller, and LocationGridFragment and LocationListFragment which act as two examples of tab content. As you may have guessed from the Fragment names, this app is going to display a collection of location data in a list view and a grid view. The location data will be very simple: an address, and a picture of the location.

You can get a copy of the code for this tutorial at GitHub: https://github.com/posco2k8/fragmenttabstutorial. If you want to see the app in action, you can download it from the Android Market... ugh... I mean "Google Play" here. I won't be providing a QR code this time around, because I've always secretly believed they were stupid. Now you know.

The Tab Controller

Here is the code for the tab controller:

tab-fragment.java
public class TabFragment extends Fragment {

    private static final int LIST_STATE = 0x1;
    private static final int GRID_STATE = 0x2;

    private int mTabState;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_tab, container, false);

        // Grab the tab buttons from the layout and attach event handlers. The code just uses standard
        // buttons for the tab widgets. These are bad tab widgets, design something better, this is just
        // to keep the code simple.
        Button listViewTab = (Button) view.findViewById(R.id.list_view_tab);
        Button gridViewTab = (Button) view.findViewById(R.id.grid_view_tab);

        listViewTab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // Switch the tab content to display the list view.
                gotoListView();
            }
        });

        gridViewTab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // Switch the tab content to display the grid view.
                gotoGridView();
            }
        });

        return view;
    }

    public void gotoListView() {
        // mTabState keeps track of which tab is currently displaying its contents.
        // Perform a check to make sure the list tab content isn't already displaying.

        if (mTabState != LIST_STATE) {
            // Update the mTabState 
            mTabState = LIST_STATE;

            // Fragments have access to their parent Activity's FragmentManager. You can
            // obtain the FragmentManager like this.
            FragmentManager fm = getFragmentManager();

            if (fm != null) {
                // Perform the FragmentTransaction to load in the list tab content.
                // Using FragmentTransaction#replace will destroy any Fragments
                // currently inside R.id.fragment_content and add the new Fragment
                // in its place.
                FragmentTransaction ft = fm.beginTransaction();
                ft.replace(R.id.fragment_content, new LocationListFragment());
                ft.commit();
            }
        }
    }

    public void gotoGridView() {
        // See gotoListView(). This method does the same thing except it loads
        // the grid tab.

        if (mTabState != GRID_STATE) {
            mTabState = GRID_STATE;

            FragmentManager fm = getFragmentManager();

            if (fm != null) {
                FragmentTransaction ft = fm.beginTransaction();
                ft.replace(R.id.fragment_content, new LocationGridFragment());
                ft.commit();
            }
        }
    }

}

The key thing to note about the above code is that Fragment's have direct access to their Activity's FragmentManager. This means a Fragment can make all the same FragmentTransactions as the Activity. This allows Fragments to interact with other Fragments from within the same Activity.

The List Tab Fragment

One of the two tab content Fragments displays a vertical list of address names. Here is the code:

location-list-fragment.java
public class LocationListFragment extends ListFragment {

    // ListFragment is a very useful class that provides a simple ListView inside of a Fragment.
    // This class is meant to be sub-classed and allows you to quickly build up list interfaces
    // in your app.

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        Activity activity = getActivity();

        if (activity != null) {
            // Create an instance of the custom adapter for the GridView. A static array of location data
            // is stored in the Application sub-class for this app. This data would normally come
            // from a database or a web service.
            ListAdapter listAdapter = new LocationModelListAdapter(activity, FragmentTabTutorialApplication.sLocations);
            setListAdapter(listAdapter);
        }
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        Activity activity = getActivity();

        if (activity != null) {   
            ListAdapter listAdapter = getListAdapter();
            LocationModel locationModel = (LocationModel) listAdapter.getItem(position);

            // Display a simple Toast to demonstrate that the click event is working. Notice that Fragments have a
            // getString() method just like an Activity, so that you can quickly access your localized Strings.
            Toast.makeText(activity, getString(R.string.toast_item_click) + locationModel.address, Toast.LENGTH_SHORT).show();
        }
    }

}

The Fragment is very straight forward, so I won't dig too deeply into it. The list is powered by a custom adapter I've written which renders the data model into a custom list item view. I won't be going into how these adapters are structured, but I like to follow the ViewHolder pattern which is explained here: http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/view/List14.html](http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/view/List14.html).

The Grid Tab Fragment

Unlike ListFragment, there is no convenient GridFragment in the Android APIs. Luckily, it is not very hard to create a similar interface. Here is the code:

location-grid-fragment.java
public class LocationGridFragment extends Fragment {

    private GridView                 mGridView;
    private LocationModelGridAdapter mGridAdapter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_location_grid, container, false);

        // Store a pointer to the GridView that powers this grid fragment.
        mGridView = (GridView) view.findViewById(R.id.grid_view);

        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Activity activity = getActivity();

        if (activity != null) {
            // Create an instance of the custom adapter for the GridView. A static array of location data
            // is stored in the Application sub-class for this app. This data would normally come
            // from a database or a web service.
            mGridAdapter = new LocationModelGridAdapter(activity, FragmentTabTutorialApplication.sLocations);

            if (mGridView != null) {
                mGridView.setAdapter(mGridAdapter);
            }

            // Setup our onItemClickListener to emulate the onListItemClick() method of ListFragment.
            mGridView.setOnItemClickListener(new OnItemClickListener() {

                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    onGridItemClick((GridView) parent, view, position, id);
                }

            });
        }
    }

    public void onGridItemClick(GridView g, View v, int position, long id) {
        Activity activity = getActivity();

        if (activity != null) {
            LocationModel locationModel = (LocationModel) mGridAdapter.getItem(position);

            // Display a simple Toast to demonstrate that the click event is working. Notice that Fragments have a
            // getString() method just like an Activity, so that you can quickly access your localized Strings.
            Toast.makeText(activity, getString(R.string.toast_item_click) + locationModel.address, Toast.LENGTH_SHORT).show();
        }
    }

}

The Activity

With the Fragments out of the way, we only have the Activity left. Of all the components in this tutorial, this is by far one of the simplest:

fragment-tab-activity.java
public class FragmentTabActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Notice how there is not very much code in the Activity. All the business logic for
        // rendering tab content and even the logic for switching between tabs has been pushed
        // into the Fragments. This is one example of how to organize your Activities with Fragments.
        // This benefit of this approach is that the Activity can be reorganized using layouts
        // for different devices and screen resolutions.
        setContentView(R.layout.activity_fragment_tab);

        // Grab the instance of TabFragment that was included with the layout and have it
        // launch the initial tab.
        FragmentManager fm = getSupportFragmentManager();
        TabFragment tabFragment = (TabFragment) fm.findFragmentById(R.id.fragment_tab);
        tabFragment.gotoListView();
    }

}

It would also be useful to see how the Activity's layout has been defined:

fragment-tab-activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <fragment
        android:id="@+id/fragment_tab"
        android:name="net.neilgoodman.android.fragmenttabstutorial.fragment.TabFragment"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <FrameLayout
        android:id="@+id/fragment_content"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>

</LinearLayout>

Update 3/13/2012: One thing I forgot to mention was how the code instantiates the Fragments. You will notice that the TabFragment is being brought into the Activity using XML, but that the tab content Fragments are dynamically added to a FrameLayout container. This is important to note, because FragmentTransaction#replace will not delete any Fragments that have been brought in using XML. This would cause problems here because the new Fragment added in a replace() call would be covering the existing XML Fragment, and both would respond to click events.

Wrapping Up

Hopefully the above code has demonstrated how simple and powerful Fragments can be. The one thing to note in the above code is that I like to push as much business logic into my Fragments as possible. This helps make them more autonomous and in turn makes them much more easy to reuse in other Activities. However, you can move as much business logic into the Activity as you like. It is really up to you.

In the next part of the tutorial I will be taking this same project, but I will implement it as a tablet UI. The challenge will be to use the larger screen space more intelligently, but not to rewrite much of my existing UI. It just so happens that this is the exact problem Fragments were designed to solve.

Let me know if you have any questions in the comments.


comments powered by