Android Fragments

One limitation of Android devices is that they can display only a single Activity at a time. This was due initially to the fact that Android was used for small screen devices such as mobile phones. With the advent of larger screen devices such as tablets, there is more space in which to display the interface. However, such devices still allow only a single Activity to be displayed at a time.

To get around this problem, starting with Android version 3 the Fragment was introduced. A Fragment is a sub-process that can be embedded within an Activity, and a single Activity can have several Fragments running at a time. A Fragment can (but need not) have a visual interface which is created in much the same way that an Activity’s interface is created.

To illustrate this, we’ll create an app that displays the two interfaces we used in the previous examples on a single screen. Start as usual by creating a new Android project with a single blank Activity in Eclipse. Eclipse doesn’t have an “Add fragment” function, but it’s still quite easy to create one. The layout of a Fragment, like that of an Activity, is defined in an XML file, so right click on the res–>layout folder in Package Explorer and select New–>Android XML file. We’ll start with the first screen that displays the Get text button we used earlier. Call this XML file get_text_fragment.xml and select RelativeLayout as its Root Element. After it’s created, open it in the Graphical Layout and add a Button and two TextViews, just as we did earlier.

Do the same for the other screen, calling the file enter_text_fragment.xml and adding a TextView, EditText and a Button.

Next we need to create Java classes to represent the Fragments and handle their events. In the src–>growe.ex04fragments (or whatever you’ve called your package) folder, right click on the folder and select New–>Class. To represent the get_text_fragment, create a class called GetTextFragment. In the Superclass box, type Fragment then Ctrl+(space), then select the Fragment class in the android.app package. (The other Fragment class in the android.support.v4.app can be used if you need your app to run on older (pre version 3) versions of Android.)

This will generate the skeleton code for your class. At this stage, we need to override only the onCreateView() method, which is most easily done in Eclipse by putting the cursor inside the class and pressing Ctrl+(space) to get a list of methods you can override. Select onCreateView() and you’ll get the code for it.

The creation of a view from an XML file requires inflating the view using a LayoutInflater. You’ll see that this is one of the arguments in onCreateView(). To create the view, delete the existing code inside this method and insert the following, so your code looks like this:

package growe.ex04fragments;

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

public class GetTextFragment extends Fragment {
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.get_text_fragment, container, false);
		return view;
	}
}

The inflate method looks up the get_text_fragment.xml file and creates a Java version of the layout.

Do the same for enter_text_fragment.xml by creating a class called EnterTextFragment which looks like this:

package growe.ex04fragments;

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

public class EnterTextFragment extends Fragment {
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.enter_text_fragment, container, false);
		Button sendTextButton = (Button) view.findViewById(R.id.sendTextButton);
		sendTextButton.setEnabled(false);
		return view;
	}
}

We’ve added a couple of lines to disable the Send text button when it is first displayed. The idea is that clicking on Get text enables this button and allows the user to send some text.

Now we need to embed these Fragments in the MainActivity and get them displayed on screen. First, we need to let the MainActivity’s layout know about the Fragments. This is done in activity_main.xml by adding in a couple of <fragment> tags. This can be done in Eclipse’s Graphical Layout.

Open activity_main.xml in Graphical Layout and open the Layouts menu in the Palette box on the left. Drag a Fragment onto the screen to where you want it. This will open a dialog in which you choose the Fragment class you want, so select GetTextFragment for the first Fragment. This will display a placeholder with instructions to pick the preview layout. To do this, right click on the placeholder and select Fragment Layout –>get_text_fragment (or open the Choose Layout dialog and choose it there). The layout components should then become visible.

Do the same to add enter_text_fragment to the layout. You should now be able to run the program in your emulator and see the combined layout.

Event handling in Fragments

Now the fun begins. We need to connect a button press in GetTextFragment to EnterTextFragment, and then send the entered text back to GetTextFragment when the Enter text button is pressed.  We could do this by simply relying on the IDs generated in the R.java file for the various view objects, but that is very bad design, for the following reason.

One of the advantages of using Fragments is that they encapsulate units of functionality, so they can be plugged in to apps wherever they are needed. If we wrote a Fragment so that it communicated directly with another Fragment, then both Fragments would always have to be present in any given app. To avoid this, we need to ensure that the only inter-Fragment communication occurs via the Activity that contains them. Doing this requires a fair bit of code, but it’s worth it to preserve modularity (or so I’m told :-)).

First, let’s see how to communicate the Get text button click event to MainActivity. Here’s the modified GetTextFragment class:

package growe.ex04fragments;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class GetTextFragment extends Fragment {

	GetTextListener activityCallback;
	View getTextView;

	public interface GetTextListener
	{
		public void onGetTextButtonPressed();
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		getTextView = inflater.inflate(R.layout.get_text_fragment, container, false);
		Button getTextButton = (Button) getTextView.findViewById(R.id.getTextButton);
		getTextButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				onClickGetText(v);
			}
		});
		return getTextView;
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		try {
			activityCallback = (GetTextListener) activity;
		} catch (ClassCastException e) {
			throw new ClassCastException(activity.toString() +
					" must implement GetTextListener");
		}
	}

	public void onClickGetText(View view) {
		activityCallback.onGetTextButtonPressed();
	}

	public void setText(String text)
	{
		TextView displayed = (TextView) getTextView.findViewById(R.id.displayedText);
		displayed.setText(text);
	}
}

On lines 26 to 33 we’ve added an event handler for getTextButton clicks. We have to do it this way rather than by simply adding an ‘On Click’ handler to the button’s properties in the Eclipse Graphical Layout screen, since the latter adds the event handler to the containing Activity class rather than the Fragment class. If this code looks a bit complicated, don’t worry, as it’s always pretty much the same so you can just copy and paste it.

Ultimately, clicking getTextButton calls onClickGetText() on line 48. To see what this does, we have to back up a bit and see how the Activity communicates with Fragments it contains.

When the Activity starts up and loads the Fragment view from the XML file, it calls the onAttach() method of the Fragment class. We need to override onAttach() to provide the connection, if we want to call a method in the Activity when an event occurs in the Fragment.

We do this by defining an interface called GetTextListener on line 17. This interface specifies a method that the Activity must implement, so that it can be called in response to the button click event. So how does this all fit together?

When the Fragment is loaded, the Activity calls onAttach(), passing a reference to itself in as an argument. We attempt to save this reference on line 41 by casting it to GetTextListener. If this succeeds, this means that the Activity does implement this interface and all is well. If it fails, we throw an exception which stops the app.

Now that we have a reference to the parent Activity, we can use it on line 49 to call the onGetTextButtonPressed() method in that Activity. It is then the Activity’s responsibility to communicate with the EnterTextFragment to enable its Send text button.

To see how this works, here’s the modified MainActivity class:

package growe.ex04fragments;

import growe.ex04fragments.EnterTextFragment.EnterTextListener;
import growe.ex04fragments.GetTextFragment.GetTextListener;
import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity
implements GetTextListener, EnterTextListener
{

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

	@Override
	public void onGetTextButtonPressed() {
		EnterTextFragment enterTextFragment =
				(EnterTextFragment) getFragmentManager().
				findFragmentById(R.id.enter_text_fragment);
		enterTextFragment.enableSendTextButton();
	}

	@Override
	public void enterTextButtonPressed() {
		EnterTextFragment enterTextFragment =
				(EnterTextFragment) getFragmentManager().
				findFragmentById(R.id.enter_text_fragment);
		GetTextFragment getTextFragment =
				(GetTextFragment) getFragmentManager().
				findFragmentById(R.id.get_text_fragment);
		getTextFragment.setText(enterTextFragment.getEnteredText());
	}

}

On line 9, we implement GetTextListener (and an interface for the other fragment, but we’ll get to that in a minute), and on line 19 we implement the onGetTextButtonPressed() method. Here, we retrieve the EnterTextFragment object and call a method (which we’ll see in a minute) in that Fragment to enable its Send text button.

Now for the revised version of EnterTextFragment:

package growe.ex04fragments;

import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;

public class EnterTextFragment extends Fragment {
	View enterTextView;
	EnterTextListener activityCallback;

	public interface EnterTextListener {
		public void enterTextButtonPressed();
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		enterTextView = inflater.inflate(R.layout.enter_text_fragment, container, false);
		Button sendTextButton = (Button) enterTextView.findViewById(R.id.sendTextButton);
		sendTextButton.setEnabled(false);
		sendTextButton.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				sendTextButtonClicked(v);
			}
		});
		return enterTextView;
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		try {
			activityCallback = (EnterTextListener) activity;
		} catch (ClassCastException e) {
			throw new ClassCastException(activity.toString() +
					" must implement EnterTextListener");
		}
	}

	public void enableSendTextButton()
	{
		Button sendTextButton = (Button) enterTextView.findViewById(R.id.sendTextButton);
		sendTextButton.setEnabled(true);
	}

	public void sendTextButtonClicked(View view)
	{
		Button sendTextButton = (Button) enterTextView.findViewById(R.id.sendTextButton);
		sendTextButton.setEnabled(false);
		activityCallback.enterTextButtonPressed();
	}

	public String getEnteredText()
	{
		EditText enteredText = (EditText) enterTextView.findViewById(R.id.enteredText);
		return enteredText.getText().toString();
	}
}

On line 47 you see the enableSendTextButton() method that is called from the Activity. Thus the event is generated in GetTextFragment which calls a method in MainActivity which in turn calls a method in EnterTextFragment. The two Fragments don’t communicate with each other directly.

To finish things off, the user enters some text in EnterTextFragment and then clicks the Send text button to send the text back to GetTextFragment. The process is much the same as the one we’ve just described, so we’ll go through it quickly.

In EnterTextFragment, we attach an event handler for the Send text button on lines 26 to 32. EnterTextFragment defines its own interface called EnterTextListener, which MainActivity must also implement. When EnterTextFragment is loaded, MainActivity is connected to it the same way (line 37), so when Send text is clicked, sendTextButtonClicked() on line 53 is called. We disable the Send text button, then call enterTextButtonPressed() in MainActivity.

If you go back to MainActivity’s code above, this method is on line 27. We need to retrieve the entered text from EnterTextFragment and send it to GetTextFragment, so we need references to both Fragments. On line 34 we call getEnteredText() from EnterTextFragment (line 60 in EnterTextFragment’s code) and pass the retrieved String to GetTextFragment’s setText() method (line 52 in GetTextFragment’s code above). Thus again, the two Fragments do not communicate directly.

This example, of course, is artificial; you’d never write an app that does something that simple in real life. But it does allow you to see how the mechanics of using Fragments work.

A more realistic example would change the layout depending on whether the device was viewed in portrait or landscape mode. We might display both fragments side by side in landscape, but only one Fragment at a time in portrait. More on that in another post.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Trackbacks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: