iX 5/2018
S. 132
Praxis
App-Entwicklung
Aufmacherbild

Googles Architecture Components für Android

App-Getriebe

Viele Android-Apps erweisen sich im Laufe der Jahre als schwer wartbar, weil ihre Entwickler wichtige Architekturprinzipien nicht beachtet haben. Mit den Android Architecture Components liefert Google endlich das Rüstzeug, es von Anfang an richtig zu machen.

Streng genommen lässt sich der Aufbau einer Android-App in wenigen Sätzen beschreiben: Activities repräsentieren fachliche Funktionseinheiten, die untereinander mit Intents kommunizieren. Für Hintergrundaktivitäten zieht der Entwickler Services heran, während die Anwendung auf Systemereignisse mit Broadcast Receivern reagiert. Content Provider schließlich gestatten den Zugriff auf tabellenartige Daten.

Listing 1: StopwatchActivity.java

package com.thomaskuenneth.stopwatch;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

public class StopwatchActivity extends Activity {

    private static final DateFormat F = new SimpleDateFormat("HH:mm:ss:SSS",
            Locale.US);
    private static final String KEY_DIFF = "diff";
    private static final String KEY_RUNNING = "running";

    private Timer timer;
    private TimerTask timerTask;
    private TextView time;
    private Button startStop;
    private Button reset;
    private long started;
    private boolean isRunning;
    private long diff;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        time = findViewById(R.id.time);
        startStop = findViewById(R.id.start_stop);
        startStop.setOnClickListener(v -> {
            isRunning = !isRunning;
            if (isRunning) {
                scheduleAtFixedRate();
            } else {
                timerTask.cancel();
            }
            updateUI();
        });
        reset = findViewById(R.id.reset);
        reset.setOnClickListener(v -> clearTime());
        if (savedInstanceState != null) {
            getValuesFromBundle(savedInstanceState);
            setTime();
        } else {
            isRunning = false;
            clearTime();
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putLong(KEY_DIFF, diff);
        outState.putBoolean(KEY_RUNNING, isRunning);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        getValuesFromBundle(savedInstanceState);
    }

    @Override
    protected void onResume() {
        super.onResume();
        timer = new Timer();
        updateUI();
        if (isRunning) {
            scheduleAtFixedRate();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        timer.cancel();
    }

    private void clearTime() {
        time.setText(R.string.cleared);
        diff = 0;
    }

    private void updateUI() {
        startStop.setText(isRunning ? R.string.stop : R.string.start);
        reset.setEnabled(!isRunning);
    }

    private void getValuesFromBundle(Bundle b) {
        diff = b.getLong(KEY_DIFF);
        isRunning = b.getBoolean(KEY_RUNNING);
    }

    private void setTime() {
        time.setText(F.format(new Date(diff)));
    }

    private void scheduleAtFixedRate() {
        started = System.currentTimeMillis() - diff;
        timerTask = new TimerTask() {
            @Override
            public void run() {
                diff = System.currentTimeMillis() - started;
                setTime();
            }
        };
        timer.scheduleAtFixedRate(timerTask, 0, 200);
    }
}

Die Nutzeroberfläche definiert der Programmierer zur Entwicklungszeit als baumartige Struktur, aus der Android zur Laufzeit einen Objektbaum macht. Was recht einfach klingt, führt schon bei kleinen Apps zu einer ganzen Menge Quelltext – wie eine einfache Stoppuhr in Listing 1 und Abbildung 1 unterstreicht. Interessierte können das gesamte Projekt vom iX-Listing-Server herunterladen.

Obwohl sie so simpel ist, führt die Stoppuhr-App die Vorteile der Architecture Components vor (Abb. 1).

Es fällt auf, dass die Activity Domänenobjekte (die Variablen started, diff und isRunning), Geschäftslogik (zum Beispiel die Methode getValuesFromBundle()) und UI-Funktionen (clearTime() und setTime()) enthält. Darüber hinaus überschreibt die App Lifecycle-Methoden, um eine Hintergrundverarbeitung zu initialisieren oder zu beenden. Nutzeroberfläche und Activity sind eng gekoppelt, weil die Instanzvariablen time, startStop und reset mehrere Bedienelemente direkt referenzieren.

Viel Code trotz kleiner Apps

Man kann sich gut vorstellen, wie der Code aussieht, wenn man zusätzliche UI-Komponenten verdrahtet oder die Geschäftslogik erweitert – die App lässt sich auf Dauer immer schwerer warten. Dabei ist die Activity nicht bewusst schlampig umgesetzt, Aufbau und Struktur orientieren sich an Googles Beispielen. Außerdem hat nicht nur Android mit der engen Kopplung und niedrigen Kohäsion zu kämpfen, dasselbe gilt für viele andere UI-Frameworks. Jedoch sorgen Entwurfsmuster wie MVC (Model View Controller), MVP (Model View Presenter) oder MVVM (Model View ViewModel) für eine klare Trennung der Zuständigkeiten und damit für bessere Wartbarkeit. Grundsätzlich hätten Entwickler von der ersten Android-Version an selbstständig diese Muster umsetzen können – nur propagierte Google sie bislang eben nicht.

Ein weiteres Problem der engen Kopplung von UI-Komponenten und Activity ist, dass man die Nutzeroberfläche nicht wiederverwenden kann. Hierzu ein Beispiel: Ein häufig eingesetztes Interaktionsmuster ist die Master-Detail-Ansicht. Aus einer Liste wählt der Nutzer ein Element aus und die App zeigt es an anderer Stelle im Detail an. Auf kleinen Smartphone-Bildschirmen setzt man dies üblicherweise mit zwei aufeinanderfolgenden Activities (Master und Detail) um. Auf Tablets ließen sich beide gleichzeitig darstellen, die parallele Anzeige mehrerer Activities setzte sich unter Android allerdings nie durch. Google erklärte die korrespondierende Klasse android.app.ActivityGroup bereits mit API-Level 13 für veraltet, denn seit Android 3 gibt es die weitaus flexibleren Fragmente. Konzeptionell ähneln diese App-Bausteine Activities: Jedes Fragment hat seinen eigenen UI-Komponentenbaum sowie einen Lebenszyklus, das Laden und Anzeigen der UI funktioniert wie bei Activities.