Samstag, 3. März 2012

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.

Kommentare:

  1. Hallo Andreas
    Studiere gerade Dein Beispiel, werde aber nicht ganz schlau draus. Die Activity "FragmentDetailActivity" scheint irgendwie "lose" im Projekt zu schweben, jedenfalls erkenne ich keine Verbindung zum Rest. Am Ende ist sie noch einmal vorhanden (also doppelt). Stimmt da was nicht??

    AntwortenLöschen
  2. Noch eine Frage:
    Habe mal angefangen, Deine App nachzuprogrammieren, bleibe aber schon beim Portrait-fragment.xml hängen. Das Fragment-View besitzt die ID "fragment_list". Die ist aber schon in der Landscape-Version vergeben. Jedenfalls meckert mein System. Schade, Dein Beispiel schien mir als Einstieg echt gut geeignet. Jetzt habe ich aber schon zwei "Hänger". Außerdem sehe ich immer noch nicht wirklich, wie die App erkennt, ob die Landscape- oder die Portrait-Version zu starten ist.
    Gruss

    AntwortenLöschen
    Antworten
    1. jep, jetzt läuft es. Habe die ID einfach umbenannt. Da auf sie im Code nicht zugegriffen wird, spielte sie keine Rolle. Die Unterscheidung zw. Portrait und Landscape ist im Click-Ereignis (das habe ich aber nicht gemeint), sondern wieso die Start-Activity "weiß", welches fragment.xml sie zu Beginn starten muss. Vlt. ist es ja so trivial, dass das System von selbst erkennt, wie das Gerät gerade "gedreht" ist. Jedenfalls läuft jetzt alles. Prima Beispiel zur Einführung, Danke!

      Löschen