This article is part of an Android Development Guide. The guide focuses on several subjects necessary to build a fully functional basic app, talking about several features that were introduced in Honeycomb and are still valid for Ice Cream Sandwich. We use a Google Buzz client called Honeybuzz as an example for each topic. Refer to the introduction for a complete list of all articles.
Honeycomb introduced the concept of Fragments. Fragments are the answer for handling different screen sizes, from phones to tablets, TVs and other gadgets. A Fragment represents a portion of the user interface. An Activity can have several Fragments and the same Fragment can be used in several Activities.
Let us consider the most common example that demonstrates its usefulness. A left pane with a listing of items and a right pane with details of the currently selected item. Such is the layout of the Honeybuzz application. A layout like this can be built with two Fragments: one for the listing of items and the other for the details of an item. Depending on the device orientation, one might display both Fragments side by side or, if in portrait mode, only one of the Fragments would be displayed (first a listing would be shown and, when an item is selected, the details replace the listing).
Let us see how to build a layout such as the one in the Honeybuzz application. Here are the different parts involved:
We basically have two different strategies and we will use them depending on the screen orientation.
The ListActivity is pretty straightforward. It is a normal Activity, but with a different layout for each screen orientation. As I explained previously, the layout in landscape mode contains both the ListFragment and DetailsFragment and the layout in portrait mode only contains the ListFragment.
Android is pretty smart in picking up the right resource file for a layout. If the device is in landscape mode, Android will first look into the layout-land folder for a resource file. If it is not available, it will try to load it from the layout folder. Likewise for portrait mode, but using the layout-port folder instead. You basically create two different layout files with the same name and place one in each folder and Android will load it appropriately according to the screen orientation.
Here is how the layout file looks like in landscape:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="fill_parent">
<fragment class="com.quasibit.honeybuzz.HoneybuzzListFragment"
android:id="@+id/listFragment"
android:layout_weight="1"
android:layout_width="@dimen/list_item_size"
android:layout_height="fill_parent" />
<fragment class="com.quasibit.honeybuzz.HoneybuzzDetailsFragment"
android:id="@+id/details"
android:layout_weight="2"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:background="?android:attr/detailsElementBackground" />
</LinearLayout>And in portrait mode:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment class="com.quasibit.honeybuzz.HoneybuzzListFragment"
android:id="@+id/listFragment"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>Moving on to the ListFragment, this will be the Fragment that displays a list of items. It can use a basic layout file. My layout only has a LinearLayout, a ListView and TextView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/listLayout"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ListView
android:id="@+id/listBuzzes"
android:drawSelectorOnTop="false"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<TextView android:id="@android:id/empty"
android:layout_height="fill_parent"
android:text="@string/no_activities"
android:layout_width="wrap_content"
android:layout_gravity="left"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp" />
</LinearLayout>The important part is the Fragment class. Here is an excerpt with the most important bits:
public class HoneybuzzListFragment extends HoneybuzzFragment implements OnItemClickListener {
protected String mCurrentId;
protected ListView mListView;
protected ArrayList<com.quasibit.honeybuzz.Buzz> mBuzzes;
protected Boolean mDualPane;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.list_fragment, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// set list view properties
this.mListView = (ListView) getActivity().findViewById(R.id.listBuzzes);
if (this.mListView != null) {
this.mListView.setOnItemClickListener(this);
this.mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
// check if we are in dual pane mode
HoneybuzzDetailsFragment details = (HoneybuzzDetailsFragment) getFragmentManager().findFragmentById(R.id.details);
mDualPane = !(details == null || !details.isInLayout());
// get current id from saved state or extras
if (savedInstanceState != null) {
// Restore last state for checked position.
mCurrentId = savedInstanceState.getString(HoneybuzzListActivity.EXTRA_ID);
}
if (this.getActivity().getIntent() != null) {
String id = this.getActivity().getIntent().getStringExtra(HoneybuzzListActivity.EXTRA_ID);
if (id != null && !id.isEmpty()) {
mCurrentId = id;
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(HoneybuzzListActivity.EXTRA_ID, mCurrentId);
}
@Override
public void onItemClick(AdapterView<?> l, View v, int position, long id) {
if (position < mBuzzes.size()) {
// get matching buzz id from position
String buzzId = mBuzzes.get(position).id;
showDetails(buzzId);
}
}
public void showDetails(String id) {
if (id != null && !id.isEmpty() && mBuzzes != null && !mBuzzes.isEmpty()) {
// find buzz object
com.quasibit.honeybuzz.Buzz buzz = getBuzz(id);
if (buzz != null) {
mCurrentId = id;
// highlight selected item
int index = mBuzzes.indexOf(buzz);
if (index >= 0) {
mListView.setItemChecked(index, true);
}
// load item
if (mDualPane) {
HoneybuzzDetailsFragment details = (HoneybuzzDetailsFragment) getFragmentManager().findFragmentById(R.id.details);
details.load(buzz);
} else {
// launch new activity because we're in single mode pane
Intent showContent = new Intent(getActivity(), HoneybuzzDetailsActivity.class);
showContent.putExtra(HoneybuzzDetailsActivity.EXTRA_BUZZ, buzz);
startActivity(showContent);
}
}
}
}
}Notice the following:
The DetailsActivity will only be used in portrait mode. In portrait mode we need both a ListActivity and a DetailsActivity, because we have two different screens and we will only show one at a time.
The DetailsActivity is pretty straightforward, we just reuse the DetailsFragment (which is used both in the ListActivity as well as in the DetailsActivity):
public class HoneybuzzDetailsActivity extends HoneybuzzActivity {
public static final String EXTRA_BUZZ = "com.quasibit.honeybuzz.HoneybuzzListActivity.EXTRA_BUZZ";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
// If the screen is now in landscape mode, we can show the
// dialog in-line with the list so we don't need this activity.
finish();
return;
}
if (savedInstanceState == null) {
// During initial setup, plug in the details fragment.
HoneybuzzDetailsFragment details = new HoneybuzzDetailsFragment();
details.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
}
}
}The DetailsFragment contains a load method which fills the layout with the data of the Buzz object.
public class HoneybuzzDetailsFragment extends HoneybuzzFragment implements OnClickListener {
protected com.quasibit.honeybuzz.Buzz mBuzz;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.details, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
// Restore last state for checked position.
mBuzz = (Buzz) savedInstanceState.getSerializable(HoneybuzzDetailsActivity.EXTRA_BUZZ);
}
if (this.getActivity().getIntent() != null) {
com.quasibit.honeybuzz.Buzz buzz = (Buzz) this.getActivity().getIntent().getSerializableExtra(HoneybuzzDetailsActivity.EXTRA_BUZZ);
if (buzz != null) {
mBuzz = buzz;
}
}
if (mBuzz != null) {
load(mBuzz);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(HoneybuzzDetailsActivity.EXTRA_BUZZ, mBuzz);
}
public void load(com.quasibit.honeybuzz.Buzz buzz) {
mBuzz = buzz;
if (buzz != null) {
// load views with Buzz object data
}
}
}In onActivityCreated we try to find a Buzz object to display, either one saved before the Activity went to the background, or one passed in the extras (called from another part of the application).