Gängige Kotlin-Muster mit Android verwenden

In diesem Thema werden einige der nützlichsten Aspekte der Kotlin-Sprache für die Android-Entwicklung behandelt.

Mit Fragmenten arbeiten

In den folgenden Abschnitten werden Fragment-Beispiele verwendet, um einige der besten Funktionen von Kotlin hervorzuheben.

Inheritance

Sie können eine Klasse in Kotlin mit dem Keyword class deklarieren. Im folgenden Beispiel ist LoginFragment eine Unterklasse von Fragment. Sie können die Vererbung angeben, indem Sie den Operator : zwischen der Unterklasse und der übergeordneten Klasse verwenden:

class LoginFragment : Fragment()

In dieser Klassendeklaration ist LoginFragment für den Aufruf des Konstruktors der Superklasse Fragment verantwortlich.

Innerhalb von LoginFragment können Sie eine Reihe von Lebenszyklus-Callbacks überschreiben, um auf Zustandsänderungen in Ihrem Fragment zu reagieren. Verwenden Sie das Schlüsselwort override, um eine Funktion zu überschreiben, wie im folgenden Beispiel gezeigt:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Wenn Sie auf eine Funktion in der übergeordneten Klasse verweisen möchten, verwenden Sie das Schlüsselwort super, wie im folgenden Beispiel gezeigt:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Null-Zulässigkeit und Initialisierung

In den vorherigen Beispielen haben einige der Parameter in den überschriebenen Methoden Typen, an die ein Fragezeichen ? angehängt ist. Das bedeutet, dass die für diese Parameter übergebenen Argumente null sein können. Achten Sie darauf, Nullwerte sicher zu verarbeiten.

In Kotlin müssen Sie die Attribute eines Objekts initialisieren, wenn Sie das Objekt deklarieren. Das bedeutet, dass Sie nach dem Abrufen einer Instanz einer Klasse sofort auf alle zugänglichen Attribute verweisen können. Die View-Objekte in einem Fragment können jedoch erst nach dem Aufrufen von Fragment#onCreateView aufgeblasen werden. Daher benötigen Sie eine Möglichkeit, die Initialisierung von Eigenschaften für ein View zu verzögern.

Mit lateinit können Sie die Initialisierung von Attributen verzögern. Wenn Sie lateinit verwenden, sollten Sie die Property so schnell wie möglich initialisieren.

Im folgenden Beispiel sehen Sie, wie lateinit verwendet wird, um View-Objekte in onViewCreated zuzuweisen:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

SAM-Conversion

Sie können Click-Events in Android erfassen, indem Sie die OnClickListener-Schnittstelle implementieren. Button-Objekte enthalten eine setOnClickListener()-Funktion, die eine Implementierung von OnClickListener akzeptiert.

OnClickListener hat eine einzelne abstrakte Methode, onClick(), die Sie implementieren müssen. Da setOnClickListener() immer ein OnClickListener als Argument verwendet und OnClickListener immer dieselbe einzelne abstrakte Methode hat, kann diese Implementierung in Kotlin mit einer anonymen Funktion dargestellt werden. Dieser Vorgang wird als Single Abstract Method-Conversion oder SAM-Conversion bezeichnet.

Durch die SAM-Konvertierung kann Ihr Code erheblich übersichtlicher werden. Im folgenden Beispiel wird gezeigt, wie Sie mit der SAM-Konvertierung eine OnClickListener für eine Button implementieren:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

Der Code in der anonymen Funktion, die an setOnClickListener() übergeben wird, wird ausgeführt, wenn ein Nutzer auf loginButton klickt.

Companion-Objekte

Begleitobjekte bieten einen Mechanismus zum Definieren von Variablen oder Funktionen, die konzeptionell mit einem Typ verknüpft sind, aber nicht an ein bestimmtes Objekt gebunden sind. Companion-Objekte ähneln der Verwendung des static-Schlüsselworts von Java für Variablen und Methoden.

Im folgenden Beispiel ist TAG eine String-Konstante. Sie benötigen keine eindeutige Instanz von String für jede Instanz von LoginFragment. Definieren Sie sie daher in einem Companion-Objekt:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

Sie könnten TAG auf der obersten Ebene der Datei definieren, aber die Datei könnte auch eine große Anzahl von Variablen, Funktionen und Klassen enthalten, die ebenfalls auf der obersten Ebene definiert sind. Companion-Objekte helfen dabei, Variablen, Funktionen und die Klassendefinition zu verknüpfen, ohne auf eine bestimmte Instanz dieser Klasse zu verweisen.

Property-Delegierung

Beim Initialisieren von Attributen wiederholen Sie möglicherweise einige der häufigsten Android-Muster, z. B. den Zugriff auf ein ViewModel innerhalb eines Fragment. Um überflüssigen doppelten Code zu vermeiden, können Sie die Syntax für die Delegierung von Eigenschaften von Kotlin verwenden.

private val viewModel: LoginViewModel by viewModels()

Die Delegierung von Attributen bietet eine gemeinsame Implementierung, die Sie in Ihrer gesamten App wiederverwenden können. Android KTX stellt einige Attributdelegaten für Sie bereit. Mit viewModels wird beispielsweise ein ViewModel abgerufen, das auf die aktuelle Fragment beschränkt ist.

Bei der Attributdelegierung wird die Spiegelung verwendet, was zu einem gewissen Leistungsaufwand führt. Der Vorteil ist eine prägnante Syntax, die Entwicklungszeit spart.

Null-Zulässigkeit

Kotlin bietet strenge Nullable-Regeln, die die Typsicherheit in Ihrer gesamten App gewährleisten. In Kotlin können Referenzen auf Objekte standardmäßig keine Nullwerte enthalten. Wenn Sie einer Variablen einen Nullwert zuweisen möchten, müssen Sie einen nullable-Variablentyp deklarieren, indem Sie ? am Ende des Basistyps hinzufügen.

Der folgende Ausdruck ist beispielsweise in Kotlin unzulässig. name ist vom Typ String und kann nicht null sein:

val name: String = null

Wenn Sie einen Nullwert zulassen möchten, müssen Sie einen Nullable-String-Typ (String?) verwenden, wie im folgenden Beispiel gezeigt:

val name: String? = null

Interoperabilität

Die strengen Regeln von Kotlin machen Ihren Code sicherer und prägnanter. Diese Regeln verringern die Wahrscheinlichkeit, dass es zu einem NullPointerException kommt, das zum Absturz Ihrer App führt. Außerdem müssen Sie weniger Nullprüfungen in Ihrem Code durchführen.

Häufig müssen Sie beim Schreiben einer Android-App auch Nicht-Kotlin-Code aufrufen, da die meisten Android-APIs in der Programmiersprache Java geschrieben sind.

Nullwerte sind ein wichtiger Bereich, in dem sich Java und Kotlin im Verhalten unterscheiden. Java ist bei der Nullable-Syntax weniger streng.

Die Klasse Account hat beispielsweise einige Attribute, darunter das Attribut String mit dem Namen name. In Java gibt es nicht die Kotlin-Regeln zur Nullable-Eigenschaft. Stattdessen wird auf optionale Nullable-Annotationen zurückgegriffen, um explizit zu deklarieren, ob ein Nullwert zugewiesen werden kann.

Da das Android-Framework hauptsächlich in Java geschrieben ist, kann es zu diesem Szenario kommen, wenn Sie APIs ohne Nullable-Annotationen aufrufen.

Plattformtypen

Wenn Sie in Kotlin auf ein nicht annotiertes name-Element verweisen, das in einer Java-Account-Klasse definiert ist, weiß der Compiler nicht, ob String in Kotlin einem String oder einem String? entspricht. Diese Mehrdeutigkeit wird durch einen Plattformtyp, String!, dargestellt.

String! hat für den Kotlin-Compiler keine besondere Bedeutung. String! kann entweder für ein String oder ein String? stehen und der Compiler ermöglicht es Ihnen, einen Wert eines der beiden Typen zuzuweisen. Wenn Sie den Typ als String darstellen und einen Nullwert zuweisen, riskieren Sie, dass eine NullPointerException ausgelöst wird.

Um dieses Problem zu beheben, sollten Sie immer Nullable-Annotationen verwenden, wenn Sie Code in Java schreiben. Diese Anmerkungen sind sowohl für Java- als auch für Kotlin-Entwickler hilfreich.

Hier ist beispielsweise die Account-Klasse, wie sie in Java definiert ist:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Eine der Membervariablen, accessId, ist mit @Nullable annotiert, was darauf hinweist, dass sie einen Nullwert enthalten kann. In Kotlin würde accessId dann als String? behandelt.

Wenn Sie angeben möchten, dass eine Variable niemals null sein kann, verwenden Sie die Annotation @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

In diesem Szenario wird name in Kotlin als nicht nullfähiger String betrachtet.

Nullability-Annotationen sind in allen neuen Android-APIs und vielen vorhandenen Android-APIs enthalten. Viele Java-Bibliotheken haben Anmerkungen zur Null-Zulässigkeit hinzugefügt, um sowohl Kotlin- als auch Java-Entwickler besser zu unterstützen.

Null-Zulässigkeit verarbeiten

Wenn Sie sich bei einem Java-Typ nicht sicher sind, sollten Sie davon ausgehen, dass er Nullwerte zulässt. Das name-Element der Klasse Account ist beispielsweise nicht annotiert. Sie sollten also davon ausgehen, dass es sich um einen nullable-Wert vom Typ String? handelt.

Wenn Sie name so kürzen möchten, dass der Wert keine führenden oder nachfolgenden Leerzeichen enthält, können Sie die trim-Funktion von Kotlin verwenden. Sie haben mehrere Möglichkeiten, eine String? sicher zu kürzen. Eine dieser Möglichkeiten ist die Verwendung des not-null-Assertionsoperators !!, wie im folgenden Beispiel gezeigt:

val account = Account("name", "type")
val accountName = account.name!!.trim()

Der Operator !! behandelt alles auf seiner linken Seite als nicht null. In diesem Fall wird name also als nicht null String behandelt. Wenn das Ergebnis des Ausdrucks links davon null ist, löst Ihre App eine NullPointerException aus. Dieser Operator ist schnell und einfach, sollte aber nur sparsam verwendet werden, da er Instanzen von NullPointerException in Ihren Code zurückbringen kann.

Eine sicherere Option ist die Verwendung des Safe-Call-Operators, ?., wie im folgenden Beispiel gezeigt:

val account = Account("name", "type")
val accountName = account.name?.trim()

Wenn name nicht null ist, ist das Ergebnis von name?.trim() mit dem Safe-Call-Operator ein Name ohne führende oder nachfolgende Leerzeichen. Wenn name null ist, ist das Ergebnis von name?.trim() null. Das bedeutet, dass Ihre App beim Ausführen dieser Anweisung niemals eine NullPointerException auslösen kann.

Der Safe-Call-Operator schützt Sie zwar vor einem potenziellen NullPointerException, übergibt aber einen Nullwert an die nächste Anweisung. Stattdessen können Sie Null-Werte sofort mit einem Elvis-Operator (?:) verarbeiten, wie im folgenden Beispiel gezeigt:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Wenn das Ergebnis des Ausdrucks auf der linken Seite des Elvis-Operators null ist, wird der Wert auf der rechten Seite accountName zugewiesen. Diese Technik ist nützlich, um einen Standardwert anzugeben, der andernfalls null wäre.

Sie können den Elvis-Operator auch verwenden, um frühzeitig aus einer Funktion zurückzukehren, wie im folgenden Beispiel gezeigt:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Android-API-Änderungen

Android-APIs werden immer Kotlin-freundlicher. Viele der gängigsten Android-APIs, darunter AppCompatActivity und Fragment, enthalten Nullable-Annotationen und für bestimmte Aufrufe wie Fragment#getContext gibt es Kotlin-freundlichere Alternativen.

Der Zugriff auf die Context eines Fragment ist beispielsweise fast immer ungleich null, da die meisten Aufrufe, die Sie in einem Fragment ausführen, erfolgen, während das Fragment an ein Activity (eine Unterklasse von Context) angehängt ist. Fragment#getContext gibt jedoch nicht immer einen Wert ungleich null zurück, da es Szenarien gibt, in denen ein Fragment nicht an ein Activity angehängt ist. Daher ist der Rückgabetyp von Fragment#getContext nullable.

Da der von Fragment#getContext zurückgegebene Context nullable ist (und als @Nullable annotiert ist), müssen Sie ihn in Ihrem Kotlin-Code als Context? behandeln. Das bedeutet, dass Sie einen der oben genannten Operatoren anwenden müssen, um die Nullable-Eigenschaft zu behandeln, bevor Sie auf ihre Eigenschaften und Funktionen zugreifen. Für einige dieser Szenarien bietet Android alternative APIs, die diese Funktionalität bereitstellen. Fragment#requireContext gibt beispielsweise ein Context zurück, das nicht null ist, und löst eine IllegalStateException aus, wenn es aufgerufen wird, wenn ein Context null wäre. So können Sie das resultierende Context als nicht null behandeln, ohne dass Safe-Call-Operatoren oder Workarounds erforderlich sind.

Attributinitialisierung

Properties in Kotlin werden nicht standardmäßig initialisiert. Sie müssen initialisiert werden, wenn ihre einschließende Klasse initialisiert wird.

Es gibt verschiedene Möglichkeiten, Eigenschaften zu initialisieren. Im folgenden Beispiel wird gezeigt, wie eine index-Variable initialisiert wird, indem ihr in der Klassendeklaration ein Wert zugewiesen wird:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Diese Initialisierung kann auch in einem Initialisierungsblock definiert werden:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

In den obigen Beispielen wird index initialisiert, wenn ein LoginFragment erstellt wird.

Es kann jedoch sein, dass einige Eigenschaften nicht während der Objekterstellung initialisiert werden können. Sie möchten beispielsweise von einem View aus auf ein Fragment verweisen. Das Layout muss also zuerst aufgebläht werden. Bei der Erstellung eines Fragment tritt keine Inflation auf. Stattdessen wird sie beim Aufrufen von Fragment#onCreateView erhöht.

Eine Möglichkeit, dieses Szenario zu beheben, besteht darin, die Ansicht als nullable zu deklarieren und sie so schnell wie möglich zu initialisieren, wie im folgenden Beispiel gezeigt:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Das funktioniert wie erwartet, aber Sie müssen jetzt die Nullable-Eigenschaft von View verwalten, wenn Sie darauf verweisen. Eine bessere Lösung ist die Verwendung von lateinit für die View-Initialisierung, wie im folgenden Beispiel gezeigt:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

Mit dem Schlüsselwort lateinit können Sie die Initialisierung einer Eigenschaft beim Erstellen eines Objekts vermeiden. Wenn auf Ihre Property verwiesen wird, bevor sie initialisiert wurde, löst Kotlin eine UninitializedPropertyAccessException aus. Initialisieren Sie Ihre Property also so schnell wie möglich.