Android Tutorial: Fragments

A powerful feature added to Android 3.0 (API level 11), fragments are components that can be embedded into an activity. Unlike custom views, fragments have their own lifecycle and may or may not have a user interface.

This chapter explains what android fragments are and shows how to use them. Related: Android Tutorial: The Activity Lifecycle

The Fragment Lifecycle

You create a fragment by extending the android.app.Fragment class or one of its subclasses. A fragment may or may not have a user interface. A fragment with no user interface (UI) normally acts as a worker for the activity the fragment is embedded into. If a fragment has a UI, it may contain views arranged in a layout file that will be loaded after the fragment is created. In many aspects, writing a fragment is similar to writing an activity.

In order to create fragments effectively, you need to know the lifecycle of a fragment. Figure 13.1 shows the lifecycle of a fragment.

The lifecycle of a fragment is similar to that of an activity. For example, it has callback methods such as onCreate, onResume and onPause. On top of that, there are additional methods like onAttach, onActivityCreated and onDetach. onAttach is called after the fragment is associated with an activity and onActivityCreated gets called after the onCreate method of the activity that contains the fragment is completed. onDetach is invoked before a fragment is detached from an activity.

Figure 1.1: The fragment lifecycle

  • onAttach: Called right after the fragment is associated with its activity.
  • onCreate: Called to create the fragment the first time.
  • onCreateView: Called when it is time to create the layout for the fragment. It must return the fragment’s root view.
  • onActivityCreated: Called to tell the fragment that its activity’s onCreate method has completed.
  • onStart: Called when the fragment’s view is made visible to the user.
  • onResume: Called when the containing activity enters the Resumed state, which means the activity is running.
  • onPause: Called when the containing activity is being paused.
  • onStop: Called when the containing activity is stopped.
  • onDestroyView: Called to allow the fragment to release resources used for its view.
  • onDestroy: Called to allow the fragment to do final clean-up before it is destroyed.
  • onDetach: Called right after the fragment is detached from its activity.

There are some subtle differences between an activity and a fragment. In an activity, you normally set the view for the activity in its onCreate method using the setContentView method, e.g.

protected void onCreate(android.os.Bundle savedInstanceState) {
    super(savedInstanceState); 
    setContentView(R.layout.activity_main);
    ...
}


In a fragment you normally create a view in its onCreateView method. Here is the signature of the onCreateViewmethod.

public View onCreateView(android.view.LayoutInflater inflater, 
        android.view.ViewGroup container, 
        android.os.Bundle savedInstanceState);

Noticed there are three arguments that are passed to onCreateView? The first argument is a LayoutInflater that you use to inflate any view in the fragment. The second argument is the parent view the fragment should be attached to. The third argument, a Bundle, if not null contains information from the previously saved state.

In an activity, you can obtain a reference to a view by calling the findViewById method on the activity. In a fragment, you can find a view in the fragment by calling the findViewById on the parent view.

View root = inflater.inflate(R.layout.fragment_names, 
        container, false);
View aView = (View) root.findViewById(id);

Also note that a fragment should not know anything about its activity or other fragments. If you need to listen for an event that occurs in a fragment that affects the activity or other views or fragments, do not write a listener in the fragment class. Instead, trigger a new event in response to the fragment event and let the activity handle it.

You will learn more about creating a fragment in later sections in this chapter.

Fragment Management

To use a fragment in an activity, use the fragment element in a layout file just as you would a view. Specify the fragment class name in the android:name attribute and an identifier in the android:id attribute. Here is an example of a fragment element.

<fragment 
    android:name="com.example.MyFragment" 
    android:id="@+id/fragment1"
    ... 
/>


Alternatively, You can manage fragments programmatically in your activity class using an android.app.FragmentManager. You can obtain the default instance of FragmentManager by calling the getFragmentManager method in your activity class. Then, call the beginTransaction method on the FragmentManager to obtain a FragmentTransaction.

FragmentManager fragmentManager = getFragmentManager(); 
FragmentTransaction fragmentTransaction = 
        fragmentManager.beginTransaction(); 

The android.app.FragmentTransaction class offers methods for adding, removing, and replacing fragments. Once you’re finished, call FragmentTransaction.commit() to commit your changes.

You can add a fragment to an activity using one of the add method overloads in the FragmentTransaction class. You have to specify to which view the fragment should be added to. Normally, you would add a fragment to a FrameLayout or some other type of layout. Here is one of the add methods in FragmentTransaction.

public abstract FragmentTransaction add(int containerViewId, 
        Fragment fragment, String tag)

To use add, you would instantiate your fragment class and then specify the ID of the view to add to. If you pass a tag, you can later retrieve your fragment using the findFragmentByTag method on the FragmentManager.

If you are not using a tag, you can use this add method.

public abstract FragmentTransaction add(int containerViewId, 
        Fragment fragment)


To remove a fragment from an activity, call the remove method on the FragmentTransaction.

public abstract FragmentTransaction remove(Fragment fragment)


And to replace a fragment in a view with another fragment, use the replace method.

public abstract FragmentTransaction replace(int containerViewId, 
        Fragment fragment, String tag) 


As a last step once you are finished managing your fragments, call commit on the FragmentTransaction.

public abstract int commit()

Using A Fragment

The FragmentDemo1 application is a sample application with an activity that uses two fragments. The first fragment lists some cities. Selecting a city causes the second fragment to show the picture of the selected city. Since proper design dictates that a fragment should not know anything about its surrounding, the first fragment rises an event upon receiving user selection. The activity handles this new event and causes the second fragment to change.

Figure 1.2 shows how FragmentDemo1 looks like.

Figure 1.2: Using fragments

The manifest for the application is printed in Listing 1.1.

Listing 1.1: The AndroidManifest.xml file for FragmentDemo1

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.example.fragmentdemo1" 
    android:versionCode="1" 
    android:versionName="1.0" > 

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

    <application 
        android:allowBackup="true" 
        android:icon="@drawable/ic_launcher" 
        android:label="@string/app_name" 
        android:theme="@style/AppTheme"> 
        <activity 
            android:name="com.example.fragmentdemo1.MainActivity" 
            android:label="@string/app_name" > 
            <intent-filter> 
                <action android:name="android.intent.action.MAIN"/> 
                <category android:name="android.intent.category.LAUNCHER"/> 
            </intent-filter> 
        </activity> 
    </application> 
</manifest>

Nothing extraordinary here. It simply declares an activity for the application.

You use a fragment as you would a view or a widget, by declaring it in an activity’s layout file or by programmatically adding one. For FragmentDemo1, two fragments are added to the layout of the application’s main activity. The layout file is shown in Listing 1.2.

Listing 1.2: The layout file for the main activity (activity_main.xml)

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <fragment 
        android:name="com.example.fragmentdemo1.NamesFragment" 
        android:id="@+id/namesFragment" 
        android:layout_weight="1" 
        android:layout_width="0dp" 
        android:layout_height="match_parent" /> 
    <fragment 
        android:name="com.example.fragmentdemo1.DetailsFragment" 
        android:id="@+id/detailsFragment" 
        android:layout_weight="2.5" 
        android:layout_width="0dp" 
        android:layout_height="match_parent" /> 
</LinearLayout>

The layout for the main activity uses a horizontal LinearLayout that splits the screen into two panes. The ratio of the pane widths is 1:2.5, as defined by the layout_weight attributes of the fragment elements. Each pane is filled with a fragment. The first pane is represented by the NamesFragment class and the second pane by the DetailsFragment class.

The first fragment, NamesFragment, gets its layout from the fragment_names.xml file in Listing 1.3. This file is located in the res/layout folder.

Listing 1.3: The fragment_names.xml file

<ListView 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@+id/listView1" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:background="#FFFF55"/>

The layout of NamesFragment is very simple. It contains a single view that is a ListView. The layout is loaded in the onCreateView method of the fragment class (See Listing 1.4).

Listing 1.4: The NamesFragment class

package com.example.fragmentdemo1;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class NamesFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, 
            ViewGroup container, Bundle savedInstanceState) {
        final String[] names = {"Amsterdam", "Brussels", "Paris"};
        // use android.R.layout.simple_list_item_activated_1
        // to have the selected item in a different color
        ArrayAdapter<String> adapter = new ArrayAdapter<String>( 
                getActivity(), 
                android.R.layout.simple_list_item_activated_1, 
                names);
        
        View view = inflater.inflate(R.layout.fragment_names, 
                container, false);
        final ListView listView = (ListView) view.findViewById(
                R.id.listView1);
        
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        listView.setOnItemClickListener(new 
                AdapterView.OnItemClickListener() { 
            @Override 
            public void onItemClick(AdapterView<?> parent, 
                    final View view, int position, long id) {
                if (callback != null) {
                    callback.onItemSelected(names[position]);
                }
            } 
        }); 
        listView.setAdapter(adapter);
        return view;
    }

    public interface Callback {
        public void onItemSelected(String id);
    }

    private Callback callback;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (activity instanceof Callback) {
            callback = (Callback) activity;
        }
    }
    @Override
    public void onDetach() {
        super.onDetach();
        callback = null;
    }
}

The NamesFragment class defines a Callback interface that its activity must implement to listen to the item selection event of its ListView. The activity can then use it to drive the second fragment. The onAttach method makes sure that the implementing class is an Activity.

The second fragment, DetailsFragment, has a layout file that is given in Listing 13.5. It contains a TextView and an ImageView. The TextView displays the name of the selected city and the ImageView shows the picture of the selected city.

Listing 1.5: The fragment_details.xml file

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical" 
    android:background="#FAFAD2" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <TextView 
        android:id="@+id/text1" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:textSize="30sp"/> 
    <ImageView 
        android:id="@+id/imageView1" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent"/> 
</LinearLayout>

The DetailsFragment class in shown in Listing 1.6. It has a showDetails method that the containing activity can call to change the content of the TextView and ImageView.

Listing 1.6: The DetailsFragment class

package com.example.fragmentdemo1;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater; 
import android.view.View;
import android.view.ViewGroup; 
import android.widget.ImageView; 
import android.widget.ImageView.ScaleType; 
import android.widget.TextView; 

public class DetailsFragment extends Fragment { 

    @Override 
    public View onCreateView(LayoutInflater inflater, 
            ViewGroup container, Bundle savedInstanceState) { 
        return inflater.inflate(R.layout.fragment_details, 
                container, false); 
    } 
    
    public void showDetails(String name) { 
        TextView textView = (TextView) 
                getView().findViewById(R.id.text1); 
        textView.setText(name); 
        
        ImageView imageView = (ImageView) getView().findViewById( 
                R.id.imageView1); 
        imageView.setScaleType(ScaleType.FIT_XY); // stretch image 
        if (name.equals("Amsterdam")) { 
            imageView.setImageResource(R.drawable.amsterdam); 
        } else if (name.equals("Brussels")) { 
            imageView.setImageResource(R.drawable.brussels); 
        } else if (name.equals("Paris")) { 
            imageView.setImageResource(R.drawable.paris); 
        } 
    } 
}

The activity class for FragmentDemo1 is presented in Listing 1.7.

Listing 1.7: The activity class for FragmentDemo1

package com.example.fragmentdemo1;
import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity 
        implements NamesFragment.Callback { 

    @Override
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
    } 
    @Override 
    public void onItemSelected(String value) { 
        DetailsFragment details = (DetailsFragment) 
                getFragmentManager().findFragmentById( 
                        R.id.detailsFragment); 
        details.showDetails(value); 
    } 
}

The most important thing to note is that the activity class implements NamesFragment.Callback so that it can capture the item click event in the fragment. The onItemSelected method is an implementation for the Callbackinterface. It calls the showDetails method in the second fragment to change the text and picture of the selected city.

Extending ListFragment and Using FragmentManager

FragmentDemo1 showed how you could add a fragment to an activity using the fragment element in the activity’s layout file. In the second sample application, FragmentDemo2, you will learn how to add a fragment to an activity programmatically.

FragmentDemo2 is similar in functionality to its predecessor with a few differences. The first difference pertains to how the name and the picture of a selected city are updated. In FragmentDemo1, the containing activity calls the showDetails method in the second fragment, passing the city name. In FragmentDemo2, when a city is selected, the activity creates a new instance of DetailsFragment and uses it to replace the old instance.

The second difference is the fact that the first fragment extends ListFragment instead of Fragment. ListFragment is a subclass of Fragment and contains a ListView that fills its entire view. When subclassing ListFragment, you should override its onCreate method and call its setListAdapter method. This is demonstrated in the NamesListFragment class in Listing 1.8.

Listing 1.8: The NamesListFragment class

package com.example.fragmentdemo2;
import android.app.Activity;
import android.app.ListFragment; 
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView; 
import android.widget.ArrayAdapter; 
import android.widget.ListView; 

/* we don't need fragment_names-xml anymore */ 
public class NamesListFragment extends ListFragment { 
    
    final String[] names = {"Amsterdam", "Brussels", "Paris"}; 
    
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        ArrayAdapter<String> adapter = new ArrayAdapter<String>( 
                getActivity(), 
                android.R.layout.simple_list_item_activated_1, 
                names); 
        setListAdapter(adapter); 
    } 
    
    @Override 
    public void onViewCreated(View view, 
            Bundle savedInstanceState) { 
        // ListView can only be accessed here, not in onCreate() 
        super.onViewCreated(view, savedInstanceState); 
        ListView listView = getListView(); 
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 
        listView.setOnItemClickListener(new 
                AdapterView.OnItemClickListener() { 
            @Override 
            public void onItemClick(AdapterView<?> parent, 
                    final View view, int position, long id) { 
                if (callback != null) { 
                    callback.onItemSelected(names[position]); 
                } 
            } 
        }); 
        
    } 
    
    public interface Callback { 
        public void onItemSelected(String id); 
    } 

    private Callback callback; 

    @Override 
    public void onAttach(Activity activity) { 
        super.onAttach(activity); 
        if (activity instanceof Callback) { 
            callback = (Callback) activity; 
        } 
    } 
    @Override 
    public void onDetach() { 
        super.onDetach(); 
        callback = null; 
    }   
}

Like the NamesFragment class in FragmentDemo1, the NamesListFragment class in FragmentDemo2 also defines a Callback interface that a containing activity must implement to listen to the ListView’s OnItemClickevent.

The second fragment, DetailsFragment in Listing 1.9, expects its activity to pass two arguments, a name and an image ID. In its onCreate method, the fragment retrieves these arguments and store them in class level variables, name and imageId. The values of the variables are then used in its onCreateView method to populate its TextViewand ImageView.

Listing 1.9: The DetailsFragment class

package com.example.fragmentdemo2;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater; 
import android.view.View;
import android.view.ViewGroup; 
import android.widget.ImageView; 
import android.widget.ImageView.ScaleType; 
import android.widget.TextView; 

public class DetailsFragment extends Fragment { 

    int imageId; 
    String name; 
    
    public DetailsFragment() { 
    } 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        if (getArguments().containsKey("name")) { 
            name = getArguments().getString("name"); 
        } 
        if (getArguments().containsKey("imageId")) { 
            imageId = getArguments().getInt("imageId"); 
        } 
    } 

    @Override 
    public View onCreateView(LayoutInflater inflater, 
            ViewGroup container, Bundle savedInstanceState) { 
        
        View rootView = inflater.inflate( 
                R.layout.fragment_details, container, false); 
        TextView textView = (TextView) 
                rootView.findViewById(R.id.text1); 
        textView.setText(name); 
        
        ImageView imageView = (ImageView) rootView.findViewById( 
                R.id.imageView1); 
        imageView.setScaleType(ScaleType.FIT_XY); //stretch image 
        imageView.setImageResource(imageId); 
        return rootView; 
    } 
}

Now that you have looked at the fragments, take a close look at the activity. The layout file is given in Listing 1.10. Instead of two fragment elements like in FragmentDemo1, the activity layout file in FragmentDemo2 has a fragment element and a FrameLayout. The latter acts as the container for the second fragment.

Listing 1.10: The activity_main.xml file

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="horizontal" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <fragment 
        android:name="com.example.fragmentdemo2.NamesListFragment" 
        android:id="@+id/namesFragment" 
        android:layout_weight="1" 
        android:layout_width="0dp" 
        android:layout_height="match_parent"/> 
    <FrameLayout 
        android:id="@+id/details_container" 
        android:layout_width="0dp" 
        android:layout_height="match_parent" 
        android:layout_weight="2.5"/> 
</LinearLayout>

The activity class for FragmentDemo2 is given in Listing 1.11. Like the activity class in FragmentDemo1, it also implements the Callback interface. However, its implementation of the onItemSelected method is different. First, it passes two arguments to the DetailsFragment. Second, every time onItemSelected is called, a new DetailsFragment instance is created and passed to the FrameLayout.

Listing 1.11: The MainActivity class

package com.example.fragmentdemo2; 
import android.app.Activity; 
import android.app.FragmentManager; 
import android.app.FragmentTransaction; 
import android.os.Bundle; 

public class MainActivity extends Activity 
        implements NamesListFragment.Callback { 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        
    } 
    @Override 
    public void onItemSelected(String value) { 
        
        Bundle arguments = new Bundle(); 
        arguments.putString("name", value); 
        if (value.equals("Amsterdam")) { 
            arguments.putInt("imageId", R.drawable.amsterdam); 
        } else if (value.equals("Brussels")) { 
            arguments.putInt("imageId", R.drawable.brussels); 
        } else if (value.equals("Paris")) { 
            arguments.putInt("imageId", R.drawable.paris); 
        } 
        DetailsFragment fragment = new DetailsFragment(); 
        fragment.setArguments(arguments); 
        FragmentManager fragmentManager = getFragmentManager(); 
        FragmentTransaction fragmentTransaction = 
                fragmentManager.beginTransaction(); 
        fragmentTransaction.replace( 
                R.id.details_container, fragment); 
        fragmentTransaction.commit(); 
    } 
}


Figure 1.3 shows FragmentDemo2.

Figure 1.3: FragmentDemo2

Summary

Fragments are components that can be added to an activity. A fragment has its own lifecycle and has methods that get called when certain phases of its life occur. In this article, you have learned to write your own fragments.

Leave a Reply

Your email address will not be published.