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.