Android Fragment-Navigierung
Willemers Informatik-Ecke

Eine Alternative zum Wechsel der Activities über Intents und startActivity ergibt sich durch die Navigation zwischen Fragments. In diesem Fall wird nur eine Activity verwendet und die Darstellung der Displays erfolgt über Fragments, die über eine Navigation per XML-Datei verbunden werden.

Das Beispiel stellt den Rumpf für ein Quizspiel dar. Es besteht der Einfachheit halber aus nur drei Bildschirmen, einem Begrüßungsbildschirm (Hello), einem für die Quizfrage (Frage) und einem für die Darstellung der richtigen Antwort (Antwort).

Projekterstellung

Das Projekt wird auf der Basis einer Empty Activity angelegt.

Anschließend wird über New | Fragment | Fragment (Blank) nacheinander je ein Fragment für die Bildschirme erstellt.

Navigations-XML erstellen

Der Wechsel zwischen den Fragments wird in einer XML-Datei festgelegt. Diese muss zunächst erzeugt werden.
  1. Auf res mit der rechten Maustaste klicken
  2. New | Android Ressource File auswählen
  3. Ein Dialog öffnet sich. Darin sind folgende Felder einzugeben:
  4. File name: navigation
  5. Resource type: Navigation
  6. Mit OK wird der Dialog geschlossen.
Es ergibt sich folgende Projektstruktur:

NavHostFragment in der Layout-Datei

Die Activity enthält keine Widgets. Diese werden später in den Fragments angelegt. Die Hauptaufgabe der Activity ist das Navigieren. Dazu wird in ihrer Layout-Datei der Container NavHostFragment abgelegt.
  1. Die Layout-Datei res/layout/activity_main.xml doppelt anklicken.
  2. Die darin befindliche TextView mit zwei Mal Taste [Entf] löschen.
  3. Aus der Palette Container, darin NavHostFragment auswählen und auf den leeren Screen ziehen.
  4. Es erscheint ein Dialog, der auf der linken Seite die Navigation namens navigation anbietet. Diese anklicken und mit OK den Dialog schließen.
  5. layout_width und layout_height in den Attributen oder im Code auf match_parent einstellen.
  6. Constraints am linken und rechten Rand nach außen ziehen.
Das Layout der MainActivity sollte anschließend etwa so aussehen:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

Anlegen der Navigationspfade

Die Pfade, auf denen die App zwischen den Fragments wechselt, werden in der Datei res/navigation/navigation.xml definiert. Dafür stellt Android-Studio einen grafischen Editor zur Verfügung, den man erreicht, indem man die Datei doppelt anklickt.

Der Editor ist zunächst leer. Über dem leeren Feld gibt es ein Rechteck mit einem grünen Plus in der rechten unteren Ecke. Wenn man darauf klickt, kann man ein Fragment auswählen, das in die Navigation einbezogen werden soll.

    Aus dem Menü fragment_hello auswählen. Es erscheint ein Rechteck im Editor. Das gleiche wird mit fragment_frage und fragment_antwort wiederholt.

    Anschließend befinden sich drei Rechtecke auf dem Bildschirm. fragment_hello trägt das Symbol eines kleinen Häuschens, ist also das Start-Fragment.

    Rechts am hello-Fragment befindet sich ein Kreis. Auf diesen klicken und ihn mit dem frage-Fragment verbinden. Anschließend auch den Kreis des fragen-Fragments mit dem antwort-Fragment verbinden und den des antwort-Fragments mit dem frage-Fragment verbinden.

    Es ergibt sich ein Bild wie das folgende:

    Im XML-Code sieht das Ergebnis so aus:

    <?xml version="1.0" encoding="utf-8"?>
    <navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/navigation"
        app:startDestination="@id/hello">
    
        <fragment
            android:id="@+id/hello"
            android:name="de.willemer.fragnav.Hello"
            android:label="fragment_hello"
            tools:layout="@layout/fragment_hello" >
            <action
                android:id="@+id/action_hello_to_frage"
                app:destination="@id/frage" />
        </fragment>
        <fragment
            android:id="@+id/frage"
            android:name="de.willemer.fragnav.Frage"
            android:label="fragment_frage"
            tools:layout="@layout/fragment_frage" >
            <action
                android:id="@+id/action_frage_to_antwort"
                app:destination="@id/antwort" />
        </fragment>
        <fragment
            android:id="@+id/antwort"
            android:name="de.willemer.fragnav.Antwort"
            android:label="fragment_antwort"
            tools:layout="@layout/fragment_antwort" >
            <action
                android:id="@+id/action_antwort_to_frage"
                app:destination="@id/frage" />
        </fragment>
    </navigation>
    
    Prinzipiell ist es auch möglich, mehrere Ziele aus einem Fragment anzugeben. Unter Code kann man die Navigationstabelle in XML-Darstellung sehen.

    Man kann für eine Verbindung eine Animation auswählen, die beim Wechsel zwischen den Fragments ablaufen soll. Dazu klickt man auf eine Verbindung. Rechts erscheint in den Attributen Animations. Dort kann beispielsweise Als enterAnim die Animation fade_in gewählt werden.

    Back-Loop

    Mit den Eigenschaften der Verbindungen lässt sich auch die Back-Loop aufbrechen. Diese Loop entsteht, wenn das Programm wieder und wieder einen Zirkel zwischen Bildschirmen durchläuft. Verwendet der Benutzer die Back-Taste, um zum vorigen Bildschirm zurückzukommen, wird er diesen Kreis immer wieder rückwärts ausführen, bis alle Runden durchlaufen sind.

    Will man das nicht, kann man unter der Eigenschaft Pop Behavior in der Klappbox popUpTo das Fragment auswählen, zu der der Rücksprung erfolgen soll. Anschließend setzt man den Wert von popUpToInclusive auf true.

    Layout

    Damit überhaupt etwas zu sehen ist, werden die Fragments mit Widgets gefüllt. Dazu werden nacheinander die Layout-Dateien der Fragments angeklickt und im Editor mit den gewünschten Elementen bestückt.

    FrameLayout nach ConstraintsLayout

    Das Layout der Fragments wird jeweils auf ConstraintLayout umgestellt.

    1. Doppelklick auf fragment_hello.xml.
    2. Im Component Tree das FrameLayout mit rechter Maustaste anklicken und Convert FrameLayout to ConstraintLayout auswählen.
    3. Den anschließenden Dialog bestätigen.
    4. Die TextView darunter löschen.

    Das gleiche erfolgt mit allen Fragment-Layouts.

    Fragment-Inhalte

    Nun müssen die Fragments noch mit den Views bestückt werden. Insbesondere die Buttons sind wichtig, da mit ihnen die Navigation angestoßen werden wird.

    • hello_fragment.xml erhält eine TextView tvHello und einen Button btHello.
    • frage_fragment.xml erhält eine TextView tvFrage. Hinzu kommt eine RadioGroup mit vier RadioButtons und zu guter Letzt ein Button btFrage
    • antwort_fragment.xml erhält eine TextView tvAntwort und einen Button btAntwort.

    Java-Sourcen

    Nun wenden wir uns den Java-Klassen zu.

    MainActivity

    Die MainActivity bleibt so wie sie ist.

    Hello

    Das Fragment Hello soll den Anwender begrüßen. Das erledigt die TextView, die im Layout mit Text versorgt werden kann.

    Zum Frage-Bildschirm soll der Anwender über den Button gelangen. Die Reaktion auf diesen Button wird in OnCreateView programmiert.

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
    
        View view = inflater.inflate(R.layout.fragment_hello, container, false);
        Button button = (Button) view.findViewById(R.id.btHello);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                NavHostFragment.findNavController(Hello.this).navigate(R.id.action_hello_to_frage);
            }
        });
        return view;
    }
    
    Der Button wird über seine Id bestimmt und mit einem Listener versehen. Die so entstehende Callback-Funktion onClick wechselt nun über das NavHostFragment der Activity mithilfe des voreingestellten Navigationspfades action_to_frage.

    Frage

    Hier kann die gleiche Vorgehensweise wie in Hello gewählt werden. Als Alternativ zeigt das Listing dieser Klasse den Weg über die Implementierung des OnClickListener durch die Klasse selbst.
    public class Frage extends Fragment implements View.OnClickListener {
    
    Da OnClickListener eine abstrakte Methode onClick enthält, muss diese implementiert werden. Diese kann durch Android-Studio automatisch erstellt werden, wenn man die rot unterstrichene Zeile mit [Alt]+[Return] auffordert.
    @Override
    public void onClick(View view) {
        NavHostFragment.findNavController(this).navigate(R.id.action_frage_to_antwort);
    }
    
    In onCreateView muss die eigene Klasse als Listener mit dem Button verknüpft werden.
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_frage, container, false);
        Button button = (Button) view.findViewById(R.id.btFrage);
        button.setOnClickListener(this);
        return view;
    }
    

    Antwort

    Das Fragment kann analog behandelt werden wie Hello oder Frage. Hier noch einmal die Variation von Hello:
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_antwort, container, false);
        Button button = (Button) view.findViewById(R.id.btAntwort);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                NavHostFragment.findNavController(Antwort.this).navigate(R.id.action_antwort_to_frage);
            }
        });
        return view;
    }
    
    Anschließend kann das Programm gestartet werden. Die Navigation sollte bereits funktionieren. Um aus dem Programm einen Quiz zu machen, bedarf es allerdings noch ein wenig Feinschliff.