Android 101: Use Fragments to handle different screen orientations

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.

Basic list and details layout with Fragments

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:

  • ListActivity: has a different layout depending on screen orientation:
    • layout: layout in landscape mode. Contains both ListFragment and DetailsFragment.
    • layout-port: layout in portrait mode. Only contains ListFragment.
  • DetailsActivity: only used in portrait mode. Contains the DetailsFragment.
  • ListFragment: when an item is clicked, depending on the screen orientation, will either update the DetailsFragment or open the DetailsActivity.

We basically have two different strategies and we will use them depending on the screen orientation.

Create the ListActivity and the ListFragment

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 class inherits from HoneybuzzFragment, which is just a Fragment with some Google Analytics tracking code and a few other helper methods.
  • onCreateView: used to inflate the proper layout.
  • onActivityCreated: determine if we are in dual pane mode and register the findings in boolean mDualPane. We also try to read the current ID both from the saved instance state (when the Activity is being recovered) and the extras data (when another part of the application requests to open a particular ID).
  • onSaveInstanceState: the current ID gets saved before the Activity goes to a background state. This ID gets recovered when the Activity is recreated.
  • showDetails: loads the Buzz object with the given ID. The important thing to notice here is how the details are shown. If we are in dual pane mode, we retrieve the HoneybuzzDetailsFragment and we pass the Buzz object. Otherwise, we launch the HoneybuzzDetailsActivity and pass the Buzz object.

Building the DetailsFragment and DetailsActivity

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).

More resources

Nuno Freitas
Posted by Nuno Freitas on December 5, 2011

Related articles