Samstag, 3. März 2012

Use of Fragments in Android

Android-Apps generally should be able to run on several different devices. Especially resolution and orientation (portrait, landscape) of an application havte to be taken into account. In particular the latter is a challenge for the developer of an application. To build an app that looks acceptable horizontally and vertically is not a trivial task - particularly if you want to avoid to much scrolling.
Introduced by Android 3.0 Honeycomb the Fragments might be helpful. Those are parts of an Activity, getting developed independently. Particularly they have a lifecycle for themselves - you find details in the android documentation. Fragments are only usable as part of an Activity.
To show you how to handle Fragments, I would like to introduce a rudimentary use case, that might serve as a base for more general cases.
What will be implemented is a list of values (first fragment) and the detail view of one selected value (second fragment). In the landscape view list and detail shall be shown beside (meaning one Activity). In the portrait view at first only the list will be seen and the selection of an element will fork into the detail view (two Activities).
The code of the base Activity is the same in both cases:

public class FragmentActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);
    }
}
The layouts have to be seperated. For landscape in /res/layout-land/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

</LinearLayout>
For portrait in /res/layout-port/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
For the detail view in the portrait mode another layout has to be created (/res/layout-port/fragment_detail_activity.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
And the Activity for the detail view has to be coded, too.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		setContentView(R.layout.fragment_detail_activity);
		Bundle extras = getIntent().getExtras();
		if (extras != null) {
			String s = extras.getString("selectedValue");
			TextView view = (TextView) findViewById(R.id.text_detail);
			view.setText(s);
		}
	}
}
The development of the list fragment looks quite simple, since Android already provides a ListFragment class. You just need to subclass this class and set its ListAdapter. A layout is not needed, as far as the standard layout is used for the list entries.
Even in this class the click event for the list entry has to be processed. This is the only place where it is necessary to distinguish the modes the Activity might be in. Depending on this in the landscape mode simply the selected value has to be shown in the detail Fragment and in portrait mode the detail Activity is started via Intent.

public class FragmentList extends ListFragment {
	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		String[] values = new String[] { "One", "Two", "Three", "Four", "Five" };
		ArrayAdapter adapter = new ArrayAdapter(getActivity(),
				android.R.layout.simple_list_item_1, values);
		setListAdapter(adapter);
	}

	@Override
	public void onListItemClick(ListView l, View v, int position, long id) {
		String item = (String) getListAdapter().getItem(position);
		FragmentDetail fragment = (FragmentDetail)getFragmentManager().findFragmentById(R.id.fragment_detail);
		if (fragment != null && fragment.isInLayout()) {
			fragment.setText(item);
		} else {
			Intent intent = new Intent(getActivity().getApplicationContext(), FragmentDetailActivity.class);
			intent.putExtra("selectedValue", item);
			startActivity(intent);

		}
	}    
}
The detail fragment implements a method to set the text.

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

	public void setText(String item) {
		TextView view = (TextView) getView().findViewById(R.id.text_detail);
		view.setText(item);
	}
}
The layout for the detail fragment is kept simple in this case (/res/layout/fragment_detail.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal|center_vertical"
        android:layout_marginTop="20dp"
        android:text="Detail"
        android:textSize="30dp" />
    
</LinearLayout>
What finally misses is the code for the detail Activity, that just needs to fetch the detail text of the Intent at start and set it on the Fragment.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		setContentView(R.layout.fragment_detail_activity);
		Bundle extras = getIntent().getExtras();
		if (extras != null) {
			String s = extras.getString("selectedValue");
			TextView view = (TextView) findViewById(R.id.text_detail);
			view.setText(s);
		}
	}
}
So far, so simple. As you can see, by the use of the Fragments their code is simply reusable. But the developer still has to maintain the layouts for the different modes.
Remark: just like the ListFragment there are PreferenceFragment and DialogFrament classes for the specific use cases.

Verwendung von Fragmenten in Android

Android-Apps erheben generell den Anspruch auf verschiedensten Devices zu laufen. Insbesondere Auflösung und Ausrichtung (Portrait, Landscape) einer Application gilt es zu unterscheiden. Für den Entwickler einer Applikation stellt gerade Letzteres eine Herausforderung dar. Eine Application sowohl horizontal, wie auch vertikal ordentlich aussehen zu lassen ist nicht trivial – insbesondere wenn man dem Nutzer Scroll-Orgien ersparen will.
Eine Hilfe dabei sind die ab Android 3.0 Honeycomb eingeführten Fragments. Dabei handelt es sich um Teile eine Activity, die eigenständig entwickelt werden können. Insbesondere besitzen sie auch einen eigenen Lifecycle – Details dazu in der Android-Doku. Fragments können aber nur im Rahmen einer Activity verwendet werden.
Um den Umgang mit Fragments zu veranschaulichen, werde ich einen rudimentären Anwendungsfall vorstellen, der als Basis für allgemeinere Fälle verwendet werden kann.
Grundlage ist eine Liste mit Werten (Fragment 1) und die Detailansicht eines ausgewählten Wertes (Fragment 2). Während in der Landscape-Sicht Liste und Detail nebeneinander dargestellt werden sollen (also in einer Activity), soll in der Portrait-Sicht zunächst nur die Liste zu sehen sein und die Auswahl eines Elementes in die Detailsicht verzweigen (zwei Activities).
Der Code der Basis-Activity ist in beiden Fällen der gleiche:

public class FragmentActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment);
    }
}
Hingegen müssen die Layouts unterschieden werden. Für Landscape in /res/layout-land/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>

</LinearLayout>
Für Portrait in /res/layout-port/fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <fragment class="de.kluck.fragment.FragmentList"
        android:id="@+id/fragment_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
Für die Detailansicht im Portrait-Layout muss ein weiteres Layout angelegt werden (/res/layout-port/fragment_detail_activity.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment class="de.kluck.fragment.FragmentDetail"
        android:id="@+id/fragment_detail"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </fragment>
</LinearLayout>
Und die Activity für die Detailansicht muss auch codiert werden:

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  setContentView(R.layout.fragment_detail_activity);
  Bundle extras = getIntent().getExtras();
  if (extras != null) {
   String s = extras.getString("selectedValue");
   TextView view = (TextView) findViewById(R.id.text_detail);
   view.setText(s);
  }
 }
}
Die Entwicklung des List-Fragments gestaltet sich ganz einfach, da Android bereits eine entsprechende ListFragment-Klasse zur Verfügung stellt. Es muss nur davon abgeleitet und ein ListAdapter gesetzt werden. Ein Layout wird nicht benötigt, sofern das Standard-Layout für die Listen-Einträge verwendet wird.
Allerdings muss in dieser Klasse auch das Click-Event für den Listen-Eintrag bearbeitet werden und das ist die Stelle, wo tatsächlich geprüft werden muss, in welchem Modus sich die zugeordnete Activity befindet. Abhängig davon wird dann im Landscape-Modus einfach nur der selektierte Wert im Detail-Fragment dargestellt bzw. im Portrait-Modus die Detail-Activity via Intent gestartet.

public class FragmentList extends ListFragment {
 @Override
 public void onActivityCreated(Bundle savedInstanceState) {
  super.onActivityCreated(savedInstanceState);
  String[] values = new String[] { "One", "Two", "Three", "Four", "Five" };
  ArrayAdapter adapter = new ArrayAdapter(getActivity(),
    android.R.layout.simple_list_item_1, values);
  setListAdapter(adapter);
 }

 @Override
 public void onListItemClick(ListView l, View v, int position, long id) {
  String item = (String) getListAdapter().getItem(position);
  FragmentDetail fragment = (FragmentDetail)getFragmentManager().findFragmentById(R.id.fragment_detail);
  if (fragment != null && fragment.isInLayout()) {
   fragment.setText(item);
  } else {
   Intent intent = new Intent(getActivity().getApplicationContext(), FragmentDetailActivity.class);
   intent.putExtra("selectedValue", item);
   startActivity(intent);

  }
 }    
}
Das Detail-Fragment bekommt eine Methode, um den Text zu setzen:

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

 public void setText(String item) {
  TextView view = (TextView) getView().findViewById(R.id.text_detail);
  view.setText(item);
 }
}
Das Layout für das Detail-Fragment ist hier auch bewusst einfach gehalten (/res/layout/fragment_detail.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_detail"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal|center_vertical"
        android:layout_marginTop="20dp"
        android:text="Detail"
        android:textSize="30dp" />
    
</LinearLayout>
Schließlich fehlt noch der Code für die Detail-Activity, der aber letztendlich nur beim Starten den Detail-Text aus dem Intent holen und dem Fragment übergeben muss.

public class FragmentDetailActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  setContentView(R.layout.fragment_detail_activity);
  Bundle extras = getIntent().getExtras();
  if (extras != null) {
   String s = extras.getString("selectedValue");
   TextView view = (TextView) findViewById(R.id.text_detail);
   view.setText(s);
  }
 }
}
So weit, so einfach. Es ist gut erkennbar, dass zwar zum einen durch die Fragmente der entsprechende Code sehr schön wiederverwendbar ist, zum anderen aber dem Entwickler nicht die Aufgabe abgenommen wird, die Layouts für die verschiedenen Modus zu pflegen.
Als Anmerkung: ähnlich dem ListFragment gibt es auch PreferenceFragment und DialogFragment Klassen für die entsprechenden Spezialfälle.