Gängige Kotlin-Muster mit Android verwenden

In diesem Thema geht es um einige der nützlichsten Aspekte der Kotlin-Sprache bei der Entwicklung für Android.

Mit Fragmenten arbeiten

In den folgenden Abschnitten werden einige der besten Funktionen von Kotlin anhand von Fragment-Beispielen hervorgehoben.

Inheritance

Eine Klasse in Kotlin kann mit dem Schlüsselwort class deklariert werden. Im folgenden Beispiel ist LoginFragment eine abgeleitete Klasse von Fragment. Sie können die Übernahme mit dem Operator : zwischen der Unterklasse und ihrer übergeordneten Klasse angeben:

class LoginFragment : Fragment()

In dieser Klassendeklaration ist LoginFragment für den Aufruf des Konstruktors seiner Basisklasse (Fragment) zuständig.

Innerhalb von LoginFragment können Sie eine Reihe von Lebenszyklus-Callbacks überschreiben, um auf Statusänderungen in Ihrer Fragment zu reagieren. Verwenden Sie zum Überschreiben einer Funktion das Schlüsselwort override, 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 das Fragezeichen ? angehängt ist. Dies bedeutet, dass die für diese Parameter übergebenen Argumente null sein können. Achten Sie darauf, die Ungültigkeit der Daten sicher zu handhaben.

In Kotlin müssen Sie die Eigenschaften eines Objekts initialisieren, wenn Sie das Objekt deklarieren. Dies bedeutet, dass Sie beim Abrufen einer Instanz einer Klasse sofort auf alle zugänglichen Attribute verweisen können. Die View-Objekte in einer Fragment können jedoch erst aufgebläht werden, wenn Fragment#onCreateView aufgerufen wird. Sie benötigen also eine Möglichkeit, die Property-Initialisierung für ein View aufzuschieben.

Mit lateinit können Sie die Initialisierung von Properties aufschieben. Wenn Sie lateinit verwenden, sollten Sie Ihre Property so schnell wie möglich initialisieren.

Das folgende Beispiel zeigt, 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

Implementieren Sie die OnClickListener-Schnittstelle, um Klickereignisse in Android zu beobachten. Button-Objekte enthalten eine setOnClickListener()-Funktion, die eine Implementierung von OnClickListener annimmt.

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

Durch SAM-Konvertierung kann Ihr Code erheblich sauberer werden. Das folgende Beispiel zeigt, wie mithilfe der SAM-Konvertierung ein OnClickListener für eine Button implementiert wird:

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 an setOnClickListener() übergebene Code in der anonymen Funktion wird ausgeführt, wenn ein Nutzer auf loginButton klickt.

Companion-Objekte

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

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

class LoginFragment : Fragment() {

    ...

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

Sie können TAG auf der obersten Ebene der Datei definieren. Die Datei kann aber auch eine große Anzahl von Variablen, Funktionen und Klassen enthalten, die auch auf der obersten Ebene definiert sind. Mit Companion-Objekten können Sie Variablen, Funktionen und die Klassendefinition verbinden, ohne auf eine bestimmte Instanz dieser Klasse zu verweisen.

Attributdelegierung

Beim Initialisieren von Attributen können Sie einige gängigere Android-Muster wiederholen, z. B. den Zugriff auf ViewModel innerhalb einer Fragment. Um übermäßigen doppelten Code zu vermeiden, können Sie die Syntax der Attributdelegierung von Kotlin verwenden.

private val viewModel: LoginViewModel by viewModels()

Die Property-Delegierung bietet eine gängige Implementierung, die Sie in Ihrer gesamten App wiederverwenden können. Android KTX bietet Ihnen einige Property-Delegierung. viewModels ruft beispielsweise eine ViewModel ab, die der aktuellen Fragment zugeordnet ist.

Bei der Property-Delegierung wird Reflexion verwendet, was zu einem gewissen Leistungsaufwand führt. Der Kompromiss ist eine prägnante Syntax, die Entwicklungszeit spart.

Null-Zulässigkeit

Kotlin bietet strenge Regeln für die Null-Zulässigkeit, die die Typsicherheit in der gesamten App gewährleisten. In Kotlin dürfen Verweise auf Objekte standardmäßig keine Nullwerte enthalten. Um einer Variablen einen Nullwert zuzuweisen, müssen Sie einen Variablentyp mit Null-Zulässigkeit deklarieren, indem Sie ? am Ende des Basistyps hinzufügen.

Der folgende Ausdruck ist beispielsweise in Kotlin nicht zulässig. name ist vom Typ String und darf keine Nullwerte enthalten:

val name: String = null

Um einen Nullwert zuzulassen, müssen Sie einen String-Typ, in dem Nullwerte zulässig sind, String? verwenden, wie im folgenden Beispiel gezeigt:

val name: String? = null

Interoperabilität

Die strengen Regeln von Kotlin sorgen dafür, dass Ihr Code sicherer und prägnanter wird. Diese Regeln verringern die Wahrscheinlichkeit, dass Ihre App durch NullPointerException abstürzt. Darüber hinaus reduzieren sie die Anzahl der Null-Prüfungen, die Sie in Ihrem Code vornehmen müssen.

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.

Null-Zulässigkeit ist ein wichtiger Bereich, in dem Java und Kotlin sich unterscheiden. Java ist mit der Syntax für Null-Zulässigkeit weniger streng.

Die Klasse Account hat beispielsweise einige Attribute, darunter ein String-Attribut namens name. In Java gibt es keine Kotlin-Regeln zur Null-Zulässigkeit. Stattdessen wird auf optionale Annotationen zur Null-Zulässigkeit zurückgegriffen, um explizit zu deklarieren, ob Sie einen Nullwert zuweisen können.

Da das Android-Framework hauptsächlich in Java geschrieben ist, kann dieses Szenario auftreten, wenn APIs ohne Annotationen zur Null-Zulässigkeit aufgerufen werden.

Plattformtypen

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

String! hat für den Kotlin-Compiler keine besondere Bedeutung. String! kann entweder einen String oder einen String? darstellen. Mit dem Compiler können Sie einen Wert eines beiden Typs zuweisen. Es besteht die Gefahr, dass ein NullPointerException ausgegeben wird, wenn Sie den Typ als String darstellen und einen Nullwert zuweisen.

Um dieses Problem zu umgehen, sollten Sie immer dann Annotationen zur Null-Zulässigkeit verwenden, wenn Sie Code in Java schreiben. Diese Annotationen sind sowohl für Java- als auch für Kotlin-Entwickler hilfreich.

Hier ist als Beispiel die Klasse Account, 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 Mitgliedsvariablen, accessId, ist mit @Nullable gekennzeichnet, was darauf hinweist, dass sie einen Nullwert enthalten kann. Kotlin würde dann accessId als String? behandeln.

Mit der Annotation @NonNull können Sie angeben, dass eine Variable nie null sein kann:

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

In diesem Szenario wird name in Kotlin als String betrachtet, die keine Nullwerte zulässt.

Annotationen zur Null-Zulässigkeit 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 handhaben

Wenn Sie sich bei einem Java-Typ nicht sicher sind, sollten Sie ihn als Nullwert betrachten. So ist beispielsweise das Mitglied name der Klasse Account nicht annotiert, daher sollten Sie davon ausgehen, dass es eine String ist, für die Nullwerte zulässig sind.

Wenn Sie name kürzen möchten, damit der Wert keine voran- oder nachgestellten Leerzeichen enthält, können Sie die trim-Funktion von Kotlin verwenden. Es gibt mehrere Möglichkeiten, ein String? sicher zu schneiden. Eine dieser Möglichkeiten besteht darin, den assertion-Operator „not-null“ (!!) zu verwenden, 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, sodass Sie in diesem Fall name als Nicht-Null-String behandeln. Wenn das Ergebnis des Ausdrucks links null ist, gibt Ihre App eine NullPointerException aus. Dieser Operator ist schnell und einfach, sollte jedoch sparsam eingesetzt werden, da er Instanzen von NullPointerException wieder in den Code einschleusen kann.

Sicherer 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() unter Verwendung des Operators "safe-call" ein Namenswert ohne voran- oder nachgestellte Leerzeichen. Wenn name null ist, ist das Ergebnis von name?.trim() null. Das bedeutet, dass Ihre App beim Ausführen dieser Anweisung niemals ein NullPointerException auslösen kann.

Der Operator „safe-call“ erspart Ihnen zwar die mögliche NullPointerException, gibt aber einen Nullwert an die nächste Anweisung weiter. Sie können stattdessen auch Null-Fälle sofort verarbeiten, indem Sie den Elvis-Operator (?:) verwenden, 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 Methode ist nützlich, um einen Standardwert anzugeben, der andernfalls null wäre.

Sie können auch den Elvis-Operator verwenden, um frühzeitig von 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

    ...
}

Änderungen an der Android API

Android APIs werden zunehmend Kotlin-freundlicher. Viele der gängigsten Android-APIs, einschließlich AppCompatActivity und Fragment, enthalten Annotationen zur Null-Zulässigkeit. Für bestimmte Aufrufe wie Fragment#getContext gibt es Kotlin-freundlichere Alternativen.

Der Zugriff auf den Context einer Fragment ist beispielsweise fast immer nicht null, da die meisten Aufrufe in einer Fragment ausgeführt werden, während die Fragment an eine Activity (eine abgeleitete Klasse 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 eine Activity angehängt ist. Daher kann der Rückgabetyp Fragment#getContext Nullwerte enthalten.

Da der von Fragment#getContext zurückgegebene Context Nullwerte enthält (und als @Nullable annotiert ist), müssen Sie ihn in Ihrem Kotlin-Code als Context? behandeln. Dies bedeutet, dass einer der oben genannten Operatoren angewendet wird, um die Null-Zulässigkeit zu beheben, bevor auf seine Attribute und Funktionen zugegriffen wird. Für einige dieser Szenarien bietet Android alternative APIs, die dies vereinfachen. Fragment#requireContext gibt beispielsweise einen Context-Wert ungleich null zurück und gibt ein IllegalStateException aus, wenn der Aufruf erfolgt, wenn ein Context-Wert null wäre. Auf diese Weise können Sie das resultierende Context als Nicht-Null behandeln, ohne dass Safe-Call-Operatoren oder Problemumgehungen erforderlich sind.

Attributinitialisierung

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

Sie können Eigenschaften auf verschiedene Arten initialisieren. Das folgende Beispiel zeigt, 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.

Möglicherweise haben Sie jedoch einige Attribute, die bei der Objekterstellung nicht initialisiert werden können. Wenn Sie beispielsweise in einem Fragment auf ein View verweisen möchten, muss das Layout zuerst aufgebläht werden. Beim Erstellen einer Fragment findet keine Inflation statt. Stattdessen wird es beim Aufrufen von Fragment#onCreateView überhöht.

Eine Möglichkeit, mit diesem Szenario umzugehen, besteht darin, die Ansicht als Nullwerte zulässig 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 zwar wie erwartet, Sie müssen jetzt aber die Ungültigkeit von View verwalten, wenn Sie darauf verweisen. Eine bessere Lösung besteht darin, für die Initialisierung von View lateinit zu verwenden, 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 lässt sich vermeiden, dass eine Eigenschaft beim Erstellen eines Objekts initialisiert wird. Wenn vor der Initialisierung auf Ihr Attribut verwiesen wird, gibt Kotlin einen UninitializedPropertyAccessException aus. Sie sollten das Attribut also so schnell wie möglich initialisieren.