Gepostet von Sergey Yeshin, starker mittlerer Android-Entwickler, DataArtMehr als anderthalb Jahre sind vergangen, seit Google die offizielle Unterstützung für Kotlin in Android angekündigt hat, und die erfahrensten Entwickler haben vor mehr als drei Jahren damit begonnen, in ihrem Kampf und nicht so in Projekten damit zu experimentieren.
Die neue Sprache wurde in der Android-Community sehr positiv aufgenommen, und die überwiegende Mehrheit der neuen Android-Projekte wird mit Kotlin an Bord beginnen. Es ist auch wichtig, dass Kotlin in einen JVM-Bytecode kompiliert wird und daher vollständig mit Java kompatibel ist. In bestehenden Android-Projekten, die in Java geschrieben wurden, gibt es auch die Möglichkeit (außerdem eine Notwendigkeit), alle Funktionen von Kotlin zu nutzen, dank derer er so viele Fans gewonnen hat.
In dem Artikel werde ich über die Erfahrungen bei der Migration einer Android-Anwendung von Java nach Kotlin sprechen, die Schwierigkeiten, die dabei überwunden werden mussten, und erklären, warum dies alles nicht umsonst war. Der Artikel richtet sich hauptsächlich an Android-Entwickler, die gerade erst anfangen, Kotlin zu lernen, und stützt sich neben der persönlichen Erfahrung auf Materialien anderer Community-Mitglieder.
Warum Kotlin?
Beschreiben Sie kurz die Funktionen von Kotlin, aufgrund derer ich im Projekt darauf umgestiegen bin und die "komfortable und schmerzlich vertraute" Java-Welt verlassen habe:
- Volle Java-Kompatibilität
- Null Sicherheit
- Typinferenz
- Erweiterungsmethoden
- Funktioniert als erstklassige und Lambda-Objekte
- Generika
- Coroutinen
- Keine geprüfte Ausnahme
DISCO App
Dies ist eine kleine Anwendung für den Austausch von Rabattkarten, die aus 10 Bildschirmen besteht. Anhand seines Beispiels werden wir die Migration betrachten.
Kurz über Architektur
Die Anwendung verwendet die MVVM-Architektur mit Google Architecture Components unter der Haube: ViewModel, LiveData, Room.
Außerdem habe ich gemäß den Prinzipien der sauberen Architektur von Onkel Bob drei Ebenen in der Anwendung ausgewählt: Daten, Domäne und Präsentation.
Wo soll ich anfangen? Wir stellen uns also die Hauptfunktionen von Kotlin vor und haben eine minimale Vorstellung von dem Projekt, das migriert werden muss. Die natürliche Frage lautet: „Wo soll ich anfangen?“.
Auf der offiziellen Dokumentationsseite
Erste Schritte mit Kotlin Android heißt es, dass Sie nur Unit-Tests schreiben müssen, wenn Sie eine vorhandene Anwendung nach Kotlin portieren möchten. Wenn Sie ein wenig Erfahrung mit dieser Sprache sammeln und einen neuen Code in Kotlin schreiben, müssen Sie nur den vorhandenen Java-Code konvertieren.
Aber es gibt ein "aber". In der Tat ermöglicht eine einfache Konvertierung normalerweise (wenn auch nicht immer), dass Sie einen funktionierenden Code für Kotlin erhalten. Die Redewendung lässt jedoch zu wünschen übrig. Außerdem werde ich Ihnen erklären, wie Sie diese Lücke aufgrund der erwähnten (und nicht nur) Merkmale der Kotlin-Sprache schließen können.
Ebenenmigration
Da die Anwendung bereits geschichtet ist, ist es sinnvoll, von oben nach Schichten zu migrieren.
Die Reihenfolge der Ebenen während der Migration ist in der folgenden Abbildung dargestellt:
Es ist kein Zufall, dass wir die Migration genau von der oberen Schicht aus begonnen haben. Wir sparen uns dadurch die Verwendung von Kotlin-Code in Java-Code. Im Gegenteil, wir lassen den Kotlin-Code der oberen Schicht die Java-Klassen der unteren Schicht verwenden. Tatsache ist, dass Kotlin ursprünglich unter Berücksichtigung der Notwendigkeit der Interaktion mit Java entwickelt wurde. Vorhandener Java-Code kann auf natürliche Weise von Kotlin aus aufgerufen werden. Wir können problemlos von vorhandenen Java-Klassen erben, darauf zugreifen und Java-Annotationen auf Kotlin-Klassen und -Methoden anwenden. Kotlin-Code kann auch in Java ohne allzu große Probleme verwendet werden, erfordert jedoch häufig zusätzlichen Aufwand, z. B. das Hinzufügen einer JVM-Annotation. Und warum unnötige Konvertierungen in Java-Code vornehmen, wenn dieser am Ende noch in Kotlin umgeschrieben wird?
Schauen wir uns als Beispiel die Überlastgenerierung an.
Wenn Sie eine Kotlin-Funktion mit Standardparameterwerten schreiben, wird diese normalerweise in Java nur als vollständige Signatur mit allen Parametern angezeigt. Wenn Sie Java-Aufrufen mehrere Überladungen bereitstellen möchten, können Sie die Annotation @JvmOverloads verwenden:
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } }
Für jeden Parameter mit einem Standardwert wird eine zusätzliche Überladung erstellt, die diesen Parameter und alle Parameter rechts davon in der Remote-Parameterliste enthält. In diesem Beispiel wird Folgendes erstellt:
Es gibt viele Beispiele für die Verwendung von JVM-Annotationen für den korrekten Betrieb von Kotlin.
Diese Dokumentationsseite beschreibt den Aufruf von Kotlin aus Java.
Nun beschreiben wir den Migrationsprozess Schicht für Schicht.
Präsentationsschicht
Dies ist eine Benutzeroberflächenebene, die Bildschirme mit Ansichten und ein ViewModel enthält, das wiederum Eigenschaften in Form von LiveData mit Daten aus dem Modell enthält. Als Nächstes betrachten wir die Tricks und Tools, die sich bei der Migration dieser Anwendungsschicht als nützlich erwiesen haben.
1. Kapt Annotation Prozessor
Wie bei jeder MVVM ist View über die Datenbindung an die ViewModel-Eigenschaften gebunden. Bei Android handelt es sich um die Android Databind Library, die die Annotationsverarbeitung verwendet. Kotlin verfügt also über
einen eigenen Anmerkungsprozessor. Wenn Sie keine Änderungen an der entsprechenden build.gradle-Datei vornehmen, wird das Projekt nicht mehr erstellt. Daher werden wir diese Änderungen vornehmen:
apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar'])
Es ist wichtig zu beachten, dass Sie alle Vorkommen der annotationProcessor-Konfiguration in Ihrem build.gradle vollständig durch kapt ersetzen müssen.
Wenn Sie beispielsweise die Dolch- oder Raumbibliotheken im Projekt verwenden, die auch den Anmerkungsprozessor unter der Haube für die Codegenerierung verwenden, müssen Sie kapt als Anmerkungsprozessor angeben.
2. Inline-Funktionen
Wenn Sie eine Funktion als Inline markieren, bitten wir den Compiler, sie am Verwendungsort zu platzieren. Der Hauptteil der Funktion wird eingebettet, dh er ersetzt die übliche Verwendung der Funktion. Dank dessen können wir die Einschränkung des Löschens des Typs umgehen, d. H. Das Löschen eines Typs. Bei Verwendung von Inline-Funktionen können wir den Typ (Klasse) zur Laufzeit abrufen.
Diese Funktion von Kotlin wurde in meinem Code verwendet, um die Klasse der gestarteten Aktivität zu "extrahieren".
inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } }
reified - Bezeichnung eines reified Typs.
In dem oben beschriebenen Beispiel haben wir auch ein Merkmal der Kotlin-Sprache wie Erweiterungen angesprochen.
3. Erweiterungen
Sie sind Erweiterungen. In Erweiterungen wurden Dienstprogrammmethoden verwendet, die dazu beitrugen, aufgeblähte und monströse Dienstprogramme für Klassen zu vermeiden.
Ich werde ein Beispiel für die Erweiterungen geben, die an der Anwendung beteiligt sind:
fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } }
Kotlin-Entwickler haben im Voraus über nützliche Android-Erweiterungen nachgedacht, indem sie ihr Kotlin Android Extensions-Plugin angeboten haben. Zu den Funktionen, die er anbietet, gehören View Binding und Parcelable Support. Detaillierte Informationen zu den Funktionen dieses Plugins finden Sie
hier .
4. Lambda-Funktionen und Funktionen höherer Ordnung
Mit den Lambda-Funktionen im Android-Code können Sie den ungeschickten ClickListener und Rückruf entfernen, die in Java über selbstgeschriebene Schnittstellen implementiert wurden.
Ein Beispiel für die Verwendung eines Lambda anstelle von onClickListener:
button.setOnClickListener({ doSomething() })
Lambdas werden auch in Funktionen höherer Ordnung verwendet, beispielsweise für Sammlungsfunktionen.
Nehmen Sie die
Karte als Beispiel:
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...}
In meinem Code gibt es eine Stelle, an der ich die ID der Karten für ihre spätere Entfernung "zuordnen" muss.
Mit dem an map übergebenen Lambda-Ausdruck erhalte ich das gewünschte ID-Array:
val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids)
Bitte beachten Sie, dass beim Aufrufen der Funktion Klammern überhaupt weggelassen werden können. Wenn Lambda das einzige Argument und das Schlüsselwort der implizite Name des einzigen Parameters ist.
5. Plattformtypen
Sie müssen unweigerlich mit den in Java geschriebenen SDKs arbeiten (einschließlich des Android SDK). Dies bedeutet, dass Sie bei Kotlin und Java Interop wie Plattformtypen immer auf der Hut sein sollten.
Ein Plattformtyp ist ein Typ, für den Kotlin keine Informationen zur Nullgültigkeit finden kann. Tatsache ist, dass Java-Code standardmäßig keine Informationen zur Gültigkeit von null
enthält und die Annotationen
NotNull und @ Nullable nicht immer verwendet werden. Wenn in Java keine entsprechende Anmerkung vorhanden ist, wird der Typ zur Plattform. Sie können damit sowohl als Typ arbeiten, der Null zulässt, als auch als Typ, der Null nicht zulässt.
Dies bedeutet, dass der Entwickler genau wie in Java für Vorgänge mit diesem Typ voll verantwortlich ist. Der Compiler fügt keine Nullprüfungslaufzeit hinzu und ermöglicht es Ihnen, alles zu tun.
Im folgenden Beispiel überschreiben wir onActivityResult in unserer Aktivität:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") }
In diesem Fall sind Daten ein Plattformtyp, der null enthalten kann. Aus Sicht des Kotlin-Codes können Daten jedoch unter keinen Umständen null sein. Unabhängig davon, ob Sie den Intent-Typ als nullfähig angeben, erhalten Sie vom Compiler keine Warnung oder keinen Fehler, da beide Versionen der Signatur gültig sind . Da der Empfang nicht leerer Daten jedoch nicht garantiert ist, da Sie dies in Fällen mit dem SDK nicht kontrollieren können, führt das Erhalten von Null in diesem Fall zu NPE.
Als Beispiel können wir auch die folgenden Stellen für die mögliche Entstehung von Plattformtypen auflisten:
- Service.onStartCommand (), wobei die Absicht null sein kann.
- BroadcastReceiver.onReceive ().
- Activity.onCreate (), Fragment.onViewCreate () und andere ähnliche Methoden.
Darüber hinaus kommt es vor, dass die Parameter der Methode mit Anmerkungen versehen werden. Aus irgendeinem Grund verliert das Studio jedoch die Nullfähigkeit, wenn eine Überschreibung generiert wird.
Domänenschicht
Diese Schicht enthält die gesamte Geschäftslogik und ist für die Interaktion zwischen der Datenschicht und der Präsentationsschicht verantwortlich. Die Schlüsselrolle spielt hier das Repository. Im Repository führen wir die erforderlichen Datenmanipulationen sowohl serverseitig als auch lokal durch. Im Obergeschoss der Präsentationsschicht geben wir nur die Repository-Schnittstellenmethode an, die die Komplexität von Datenoperationen verbirgt.
Wie oben angegeben, wurde RxJava für die Implementierung verwendet.
1. RxJava
Kotlin ist voll kompatibel mit RxJava und in Verbindung damit prägnanter als Java. Aber auch hier musste ich mich einem unangenehmen Problem stellen. Es klingt so: Wenn Sie ein Lambda als Parameter der
andThen- Methode übergeben,
funktioniert dieses Lambda nicht!
Um dies zu überprüfen, schreiben Sie einfach einen einfachen Test:
Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe()
Und dann wird der Inhalt fehlschlagen. Dies ist bei den meisten Operatoren der Fall (wie
flatMap ,
defer ,
fromAction und viele andere). Als Argumente wird tatsächlich Lambda erwartet. Und mit einem solchen Datensatz mit
andThen wird
Completable / Observable / SingleSource erwartet . Das Problem wird gelöst, indem gewöhnliche Klammern () anstelle von geschweiftem {} verwendet werden.
Dieses Problem wird im Artikel
„Kotlin und Rx2. Wie ich 5 Stunden wegen falscher Klammern verschwendet habe .
"2. Umstrukturierung
Wir gehen auch auf eine so interessante Kotlin-Syntax wie Destrukturierung oder
Destrukturierungszuweisung ein . Sie können ein Objekt mehreren Variablen gleichzeitig zuweisen und es in Teile zerlegen.
Stellen Sie sich vor, wir haben eine Methode in der API, die mehrere Entitäten gleichzeitig zurückgibt:
@GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?)
Eine kompakte Methode, um das Ergebnis dieser Methode zurückzugeben, ist die Destrukturierung, wie im folgenden Beispiel gezeigt:
syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } }
Erwähnenswert ist, dass Mehrfachdeklarationen auf Konventionen basieren: Klassen, die zerstört werden sollen, müssen Komponenten-N () -Funktionen enthalten, wobei N die entsprechende Komponentennummer ist - ein Mitglied der Klasse. Das obige Beispiel wird in den folgenden Code übersetzt:
val cards = it.component1() val brands = it.component2()
In unserem Beispiel wird eine Datenklasse verwendet, die automatisch die Funktionen componentN () deklariert. Daher funktionieren Multi-Deklarationen sofort mit ihm.
Wir werden im nächsten Teil, der der Datenschicht gewidmet ist, mehr über die Datenklasse sprechen.
Datenschicht
Diese Schicht enthält POJO für Daten vom Server und der Basis sowie Schnittstellen für die Arbeit mit lokalen Daten und vom Server empfangenen Daten.
Für die Arbeit mit lokalen Daten wurde Room verwendet, das uns einen praktischen Wrapper für die Arbeit mit der SQLite-Datenbank bietet.
Das erste Ziel für die Migration, das sich anbietet, sind POJOs, die im Standard-Java-Code Massenklassen mit vielen Feldern und den entsprechenden get / set-Methoden sind. Sie können POJO mithilfe von Datenklassen präziser gestalten. Eine Codezeile reicht aus, um eine Entität mit mehreren Feldern zu beschreiben:
data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String)
Zusätzlich zur Prägnanz erhalten wir:
- Überschreibt die Methoden equals () , hashCode () und toString () unter der Haube. Das Generieren von Gleichheiten für alle Eigenschaften der Datenklasse ist äußerst praktisch, wenn DiffUtil in einem Adapter verwendet wird, der Ansichten für RecyclerView generiert. Tatsache ist, dass DiffUtil zwei Datensätze vergleicht, zwei Listen: die alte und die neue, herausfindet, welche Änderungen aufgetreten sind, und mithilfe von Benachrichtigungsmethoden den Adapter optimal aktualisiert. In der Regel werden Listenelemente mit Gleichheit verglichen.
Nachdem Sie der Klasse ein neues Feld hinzugefügt haben, müssen Sie es nicht gleich hinzufügen, damit DiffUtil das neue Feld berücksichtigt. - Unveränderliche Klasse
- Unterstützung für Standardwerte, die durch die Verwendung des Builder-Musters ersetzt werden können.
Ein Beispiel:
data class Card(val id : Long = 0L, val cardNumber: String="99", val barcode: String = "", var brandId: String="1") val newCard = Card(id =1L,cardNumber = "123")
Eine weitere gute Nachricht: Wenn kapt konfiguriert ist (wie oben beschrieben), funktionieren Datenklassen problemlos mit Raumanmerkungen, sodass Sie alle Datenbankentitäten in Datenklassen übersetzen können. Room unterstützt auch nullfähige Eigenschaften. Zwar unterstützt Room die Standardwerte von Kotlin noch nicht, aber der entsprechende Fehler wurde bereits dafür eingerichtet.
Schlussfolgerungen
Wir haben nur einige Fallstricke untersucht, die während der Migration von Java nach Kotlin auftreten können. Es ist wichtig, dass Probleme, insbesondere wenn es an theoretischen Kenntnissen oder praktischen Erfahrungen mangelt, alle lösbar sind.
Das Vergnügen, in Kotlin prägnanten, ausdrucksstarken und sicheren Code zu schreiben, wird jedoch alle Schwierigkeiten, die auf dem Übergangspfad auftreten, mehr als auszahlen. Ich kann mit Zuversicht sagen, dass das DISCO-Projektbeispiel dies sicherlich bestätigt.
Bücher, nützliche Links, Ressourcen
- Die theoretische Grundlage der Sprachkenntnisse wird es ermöglichen, das Buch Kotlin in Aktion von den Schöpfern der Sprache Svetlana Isakova und Dmitry Zhemerov zu legen.
Laconicism, Informativität, breite Themenabdeckung, Fokus auf Java-Entwickler und die Verfügbarkeit einer russischen Version machen es zu Beginn des Sprachenlernens zum besten der möglichen Handbücher. Ich habe mit ihr angefangen. - Kotlin- Quellen mit developer.android.
- Kotlin Guide auf Russisch
- Ein ausgezeichneter Artikel von Konstantin Mikhailovsky, einem Android-Entwickler von Genesis, über die Erfahrungen beim Wechsel zu Kotlin.