Working with Fragments and ViewPager on Android

android android-dev android-fragments android-viewpager

October 20 2013 - 06:21


First of all, you should always read the docs, all the content you need to know you will find there. I know, there are sometimes we just want to see an example, grab the code and start coding based on the example, but it might create bad habits on us, and sometimes lead us to wrong programming practices. So my advise is to follow the tutorial posts, but also read the documentations.

Introduction

So, according to the documentation, a Fragment:

represents a behavior or a portion of user interface in an Activity. You can combine multiple fragments in a single activity to build a multi-pane UI and reuse a fragment in multiple activities. You can think of a fragment as a modular section of an activity, which has its own lifecycle, receives its own input events, and which you can add or remove while the activity is running.

So, you can think of a fragment as a subactivity, that you can reuse in different activities.

Remember that a fragment must always be embedded of an activity and that the fragment's lifecycle is directly affected by the host activity's lifecycle, for example when an Activity is paused, so are all fragments.

When you add a fragment as a part of an activity layout, it lives in a ViewGroup inside the activity's view hierarchy and the fragment defines its own view layout.

The Design Philosophy

Fragments were introduced on Android 3.0 (API Level 11), primarily to support more dynamic and flexible UI designs on large screens such as tablets. By dividing the layout of an activity into fragments, you become able to modify the activity's appearance at runtime and preserve those changes in a back stack that's managed by the activity.

For example, a news application can use one fragment to show a list of articles on the left and another fragment to display an article on the right—both fragments appear in one activity, side by side, and each fragment has its own set of lifecycle callback methods and handle their own user input events. Thus, instead of using one activity to select an article and another activity to read the article, the user can select an article and read it all within the same activity, as illustrated in the tablet layout in figure 1.


Figure 1. An example of how two UI modules defined by fragments can be combined into one activity for a tablet design, but separated for a handset design.


The Sample Application

OK. Let's start our sample application, so you will learn how to implement it. Here we are just going implement a small application that will display a fragment with a ListView and a ViewPager with a fragment per page. It's pretty simple but will teach you the concept. So the first step is to create your application. As we want to support older versions, we will have to use the support library, which you can download and learn how to set it up here.

Technical Observation: in this tutorial I'm using Android Studio, but you can use whatever IDE you like.

For this application to support 2.2+ versions, you must set your application with minimum sdk version=8 and target sdk version=18 (in this case we want to reach the jelly bean version, but use the target version you want), so in your AndroidManifest.xml file, you must have this:

<uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="18" />

Create the Host Activity

Now we must create the host Activity, that will host both fragments. Create a class called MainActivity.java. This class must extend the FragmentActivity class from the support library.

package com.rogcg.fragmentssample;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.Menu;

public class MainActivity extends FragmentActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {   
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

Also, we must create the layout for this Activity, that will hold a fragment element tag. So, create a layout for the activity called activity_main.xml. Here is the code for the layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin">

    <fragment android:name="com.rogcg.fragmentssample.FirstFragment"
        android:id="@+id/firstFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#FF0000"
        tools:layout="@layout/first_fragment" />

</LinearLayout>

As you can see, there is a <fragment> tag declared, representing a fragment, also, it has it's own layout particularities. The android:name attribute in the <fragment> specifies the Fragment class to instantiate in the layout. When the system creates this activity layout, it instantiates each fragment specified in the layout and calls the onCreateView() method for each one, to retrieve each fragment's layout. The system inserts the View returned by the fragment directly in place of the <fragment> element.


Creating the First Fragment

To create a Fragment you must extend the class Fragment (or an existing subclass of it). The Fragment class has code that looks a lot like an Activity. It contains callback methods similar to an activity, such as onCreate(), onStart(), onPause(), and onStop(). In fact, if you're converting an existing Android application to use fragments, you might simply move code from your activity's callback methods into the respective callback methods of your fragment.

There are some other subtypes that you can extend, like, DialogFragment(displays a floating dialog), PreferenceFragment(displays a hierarchy of Preference objects as a list, similar to PreferenceActivity. This is useful when creating a "settings" activity for your application.) or ListFragment(displays a list of items that are managed by an adapter (such as a SimpleCursorAdapter), similar to ListActivity).

In this case we are going to use ListFragment since we want to implement a ListView in our fragment.

So, create a class called FirstFragment.java. Remember to extend the ListFragment class from android.support.v4.app.ListFragment, or your app won't support older versions. After that, we must implement the main methods.

 package com.rogcg.fragmentssample;

 import android.app.Activity;
 import android.os.Bundle;
 import android.support.v4.app.ListFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;

 import java.util.ArrayList;

 public class FirstFragment extends ListFragment
 {
     private ArrayList<String> itemsArray = new ArrayList<String>();
     private ArrayAdapter<String> adapter;

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

         itemsArray.add("Item 1");
         itemsArray.add("Item 2");
         itemsArray.add("Item 3");
         itemsArray.add("Item 4");
         itemsArray.add("Item 5");
         itemsArray.add("Item 6");
         itemsArray.add("Item 7");
         itemsArray.add("Item 8");
         itemsArray.add("Item 9");
         itemsArray.add("Item 10");
         itemsArray.add("Item 11");
         itemsArray.add("Item 12");
         itemsArray.add("Item 13");
         itemsArray.add("Item 14");
         itemsArray.add("Item 15");
         itemsArray.add("Item 16");
         itemsArray.add("Item 17");
         itemsArray.add("Item 18");
         itemsArray.add("Item 19");
         itemsArray.add("Item 20");
     }

     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
     {
         return inflater.inflate(R.layout.first_fragment, container, false);
     }

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

         // Populates list with our static array
         adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, itemsArray);
         setListAdapter(adapter);
     }

     @Override
     public void onAttach(Activity activity)
     {
         super.onAttach(activity);
     }

     @Override
     public void onStart()
     {
         super.onStart();
     }

     @Override
     public void onResume()
     {
         super.onResume();
     }
}

As you can see, the fragment life cycle looks a lot like an Activity. The onCreate(), onResume() methods are present, and we also have some specific methods for fragments like onCreateaView(), onAttach().

OK, now lets create the layout for our FirstFragment. Create a xml layout file called first_fragment.xml. Here you can build whatever you want. Since the fragment has its own life cycle, you can add, remove and handle any UI element and data you want, that it will work separated from everything in the Activity (but remember that the Fragment is attached to the Activity life cycle).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/first_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="First Fragment"
        android:id="@+id/firstFragmentTextView"
        android:layout_gravity="center_horizontal|top" />

    <ListView
        android:id="@id/android:list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

As we want to implement a ListView on this fragment, we must declare it in our layout. Also we are setting a TextView to be a title for the fragment.


Creating the ViewPager

OK, now we are going to work on the ViewPager, back to the MainActivity, we must implement something new there. Starting from the layout, we must add the TabHost, TabWidget, a FrameLayout and a ViewPager. So let's change the activity_main.xml a little bit. Here is the end result.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical">

    <fragment android:name="com.rogcg.fragmentssample.FirstFragment"
        android:id="@+id/firstFragment"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="#FF0000"
        tools:layout="@layout/first_fragment" />

    <TabHost
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TabWidget
                android:id="@android:id/tabs"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0"/>

            <FrameLayout
                android:id="@android:id/tabcontent"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_weight="0"/>

            <android.support.v4.view.ViewPager
                android:id="@+id/pager"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"/>

        </LinearLayout>
    </TabHost>

</LinearLayout>

So, as you can see, the <fragment> tag is still there, and we created something new. To create a ViewPager we must use the TabHost, which is a container for a tabbed window view. This object holds two children: a set of tab labels that the user clicks to select a specific tab, and a FrameLayout object that displays the contents of that page.

OK, now in our MainActivity code, let's add some attributes and declare our TabAdapter class. So here is the end result.

package com.rogcg.fragmentssample;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TabHost;
import android.widget.TabWidget;

import java.util.ArrayList;

public class MainActivity extends FragmentActivity
{
    private TabHost mTabHost;
    private ViewPager mViewPager;
    private TabsAdapter mTabsAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();

        mViewPager = (ViewPager)findViewById(R.id.pager);
        mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);

        mTabsAdapter.addTab(mTabHost.newTabSpec("one").setIndicator("One"), PageOneFragment.class, null);
        mTabsAdapter.addTab(mTabHost.newTabSpec("two").setIndicator("Two"), PageTwoFragment.class, null);

        if (savedInstanceState != null)
        {
            mTabHost.setCurrentTabByTag(savedInstanceState.getString("tab"));
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    public static class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener
    {
        private final Context mContext;
        private final TabHost mTabHost;
        private final ViewPager mViewPager;
        private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();

        static final class TabInfo
        {
            private final String tag;
            private final Class<?> clss;
            private final Bundle args;

            TabInfo(String _tag, Class<?> _class, Bundle _args)
            {
                tag = _tag;
                clss = _class;
                args = _args;
            }
        }

        static class DummyTabFactory implements TabHost.TabContentFactory
        {
            private final Context mContext;

            public DummyTabFactory(Context context)
            {
                mContext = context;
            }

            public View createTabContent(String tag)
            {
                View v = new View(mContext);
                v.setMinimumWidth(0);
                v.setMinimumHeight(0);
                return v;
            }
        }

        public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager)
        {
            super(activity.getSupportFragmentManager());
            mContext = activity;
            mTabHost = tabHost;
            mViewPager = pager;
            mTabHost.setOnTabChangedListener(this);
            mViewPager.setAdapter(this);
            mViewPager.setOnPageChangeListener(this);
        }

        public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args)
        {
            tabSpec.setContent(new DummyTabFactory(mContext));
            String tag = tabSpec.getTag();

            TabInfo info = new TabInfo(tag, clss, args);
            mTabs.add(info);
            mTabHost.addTab(tabSpec);
            notifyDataSetChanged();
        }

        @Override
        public int getCount()
        {
            return mTabs.size();
        }

        @Override
        public Fragment getItem(int position)
        {
            TabInfo info = mTabs.get(position);

            return Fragment.instantiate(mContext, info.clss.getName(), info.args);

        }

        public void onTabChanged(String tabId)
        {
            int position = mTabHost.getCurrentTab();
            mViewPager.setCurrentItem(position);
        }

        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
        {
        }

        public void onPageSelected(int position)
        {
            // Unfortunately when TabHost changes the current tab, it kindly
            // also takes care of putting focus on it when not in touch mode.
            // The jerk.
            // This hack tries to prevent this from pulling focus out of our
            // ViewPager.
            TabWidget widget = mTabHost.getTabWidget();
            int oldFocusability = widget.getDescendantFocusability();
            widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
            mTabHost.setCurrentTab(position);
            widget.setDescendantFocusability(oldFocusability);
        }

        public void onPageScrollStateChanged(int state)
        {
        }
    }
}

As you can see, there is a new class called TabsAdapter. This is a helper class that implements the management of tabs and all details of connecting a ViewPager with associated TabHost. It relies on a trick.
Normally a tab host has a simple API for supplying a View or Intent that each tab will show. This is not sufficient for switching between pages. So instead we make the content part of the tab host 0dp high (it is not shown) and the TabsAdapter supplies its own dummy view to show as the tab content. It listens to changes in tabs, and takes care of switch to the correct paged in the ViewPager whenever the selected tab changes.

Also, as you have seen, we have added two more fragments, one for each page in the tabsAdapter by calling mTabsAdapter.addTab in onCreate() method. This will make sure, that in each tab, the correct fragments are loaded.


Creating two more fragments for the ViewPager

Now we are going to create the two fragments that are loaded in the ViewPager. Both are just the same, so, create two classes called PageOneFragment.java and PageTwoFragment.java respectively.

Here is the code for PageOneFragment.java.

package com.rogcg.fragmentssample;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class PageOneFragment extends Fragment
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.pageone_fragment, container, false);
    }

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

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
    }

    @Override
    public void onStart()
    {
        super.onStart();
    }

    @Override
    public void onResume()
    {
        super.onResume();
    }
}

As you can see, it inflates the layout pageone_fragment.xml, so let's create it.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/first_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:background="#ff4063ff">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="First Page"
        android:textColor="#FFFFFF"
        android:textStyle="bold"
        android:id="@+id/firstFragmentTextView"
        android:layout_gravity="center_horizontal|top" />

</LinearLayout>

Now, let's create the PageTwoFragment.java class. Here is the code.

package com.rogcg.fragmentssample;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class PageTwoFragment extends Fragment
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.pagetwo_fragment, container, false);
    }

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

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
    }

    @Override
    public void onStart()
    {
        super.onStart();
    }

    @Override
    public void onResume()
    {
        super.onResume();
    }
}

And it's respective layout called pagetwo_fragment.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/first_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:background="#ff656565">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Second Page"
        android:textColor="#FFFFFF"
        android:textStyle="bold"
        android:id="@+id/firstFragmentTextView"
        android:layout_gravity="center_horizontal|top" />

</LinearLayout>

As you can see, the only thing that differs between these fragments are the colors and the TextView text. This is because the main intention of this post is to show the concept and how to work with Fragments and ViewPager together, and not how to build a cool UI.


The final result

So running the application on different versions, 2.2 (Nexus One) and 4.3 (Nexus 4), we have it working. As you can see the tabs on version 2.2 are very different from 4.3. This is because 2.2 doesn't have the Holo theme built in by default, and you have to customize it and use which theme you prefer.

You can see that the elements on the page run independently from each other, and that you can add multiple fragments to an Activity, and build their structure different from other fragments. Also, you can add multiple fragments for a ViewPager by adding new tabs. Pretty cool, huh?



























Download Source Code

Feel free to contribute to this post with your opinions, questions, etc.