Um eine qualitativ hochwertige Anwendung zu erstellen, muss man der App-Architektur große Aufmerksamkeit schenken, da ihre Rolle entscheidend ist. Tatsächlich basieren die grundlegendsten Entscheidungen, die die Struktur und Interaktion der Komponenten bestimmen, auf der Architektur. Daher ist die Wahl der geeigneten Architektur einer der Schlüsselpunkte beim Aufbau eines anständigen Produkts. Damit die Anwendung zuverlässig, skalierbar und leicht testbar ist, müssen von Anfang an die Prinzipien ihres Betriebs festgelegt werden. Dies hilft, die starre Verknüpfung der Elemente zu beseitigen, den Code leicht lesbar und modifizierbar zu machen und ein Gleichgewicht zwischen Qualität und Entwicklungsgeschwindigkeit zu gewährleisten.
In dem Artikel, den wir den Lesern vorstellen möchten, betrachten wir eine der Möglichkeiten zur Implementierung einer hochwertigen Architektur mit dem Stack aus Kotlin (Entwicklungssprache), MVVM (Logik der Anwendungskomponenten-Interaktion), Koin (Dependency Injection) und Coroutines (Multithreading). Warum dieser Stack? Kotlin ist die offiziell empfohlene Google-Sprache für die Android-Entwicklung. Im Vergleich zu Java eliminiert es eine große Anzahl von Boilerplate-Code, hat einen vorteilhaften Ansatz zur Null-Sicherheit, Datenklassen usw. MVVM (Model-View-ViewModel) ist ein Architekturmuster, das es ermöglicht, UI, Geschäftslogik und Datenquellen zu trennen. Koin ist eine leichte Bibliothek mit einer Reihe praktischer Funktionen zur Implementierung von Dependency Injection. Es erleichtert das Erstellen von Singletons, das Erstellen benutzerdefinierter Bereiche und das Injizieren von Abhängigkeiten. Schließlich sind Coroutines eine interessante Funktion von Kotlin für Multithreading. Sie können als leichte Threads betrachtet werden, deren Erstellung nicht viele Ressourcen erfordert.
Unsere Testanwendung wird „Dog Explorer“ heißen und eine Liste verschiedener Hunderassen anzeigen. Wir werden Daten aus der Ressource https://api.thedogapi.com/ abrufen.
Erstellen Sie zunächst ein neues Projekt in der Android Studio IDE. Wählen Sie unter den vorgeschlagenen Vorlagen leere Aktivität. Die Entwicklungssprache ist Kotlin; die minimale API-Stufe ist 21 (Android 5); use androidx artifacts ist wahr.
Verbinden Sie die notwendigen Abhängigkeiten für das Projekt. Fügen Sie zuerst die Koin-Bibliothek hinzu. Fügen Sie in die build.gradle-Datei (App-Modul) die folgenden Zeilen ein:
1 2 3 4 5 |
implementation "org.koin:koin-core:$koin_version" implementation "org.koin:koin-core-ext:$koin_version" implementation "org.koin:koin-androidx-ext:$koin_version" |
Die Variable $koin_version wird in build.gradle des gesamten Projekts gespeichert. Nach ext.kotlin_version fügen Sie die Zeile ext.koin_version = ‘2.0.1’ hinzu. Beachten Sie, dass das Projekt die zweite Version von Koin verwendet.
Für Serveranfragen verwenden wir die Bibliotheken OkHttp und Retrofit 2. Fügen Sie nach demselben Prinzip zu build.gradle (App-Modul) hinzu:
1 2 3 4 5 6 7 |
implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.okhttp3:logging-interceptor:$logging_interceptor_version" implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutine_adapter_version" |
und in build.gradle des gesamten Projekts
1 2 3 4 5 |
ext.retrofit_version = '2.6.0' ext.logging_interceptor_version = '3.12.1' ext.coroutine_adapter_version = '0.9.2' |
Wir synchronisieren die Gradle-Dateien.
Nun können wir die ersten Injektionen vorbereiten. In Koin ist der Objektanbieter die Klasse Module, die mithilfe einer Funktion mit dem unerwarteten Namen module() erstellt wird. Um sie zu erstellen, nutzen wir die wunderbare Fähigkeit von Kotlin – Paketebene-Funktionen und -Variablen. Im Gegensatz zu Java erlaubt Kotlin das Speichern von Funktionen und Variablen nicht nur in Klassen, sondern auch in Dateien. Im dogexplorer-Paket erstellen wir ein neues di-Paket und darin die Datei appModule. In der Datei erstellen wir ein Objekt der Klasse Module:
1 2 3 |
val appModule = module {} |
Die Namen der Datei und der Variablen müssen nicht übereinstimmen. In unserem Projekt wird dies der Einfachheit und Verständlichkeit halber so gemacht.
Zum Arbeiten mit dem Netzwerk benötigen wir 3 Klassen: OkHttpClient (für die tatsächlichen Anfragen), Retrofit (für die bequeme Konvertierung unserer Objekte zu JSON und umgekehrt) und das NetworkApi-Interface (in dem wir Serveranfragen definieren).
Erstellen Sie 3 Funktionen. Die erste gibt eine OkHttpClient-Instanz mit dem verbundenen Logging-Interceptor zurück – eine vorteilhafte Sache zur Kontrolle der gesendeten und empfangenen Daten.
1 2 3 4 5 6 7 8 |
fun createOkHttpClient(): OkHttpClient { val httpLoggingInterceptor = HttpLoggingInterceptor() httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY return OkHttpClient.Builder() .addInterceptor(httpLoggingInterceptor).build() } |
Die zweite erstellt eine Retrofit-Instanz. Wir geben Gson als Konverter unserer Objekte zu JSON und umgekehrt an, sowie CoroutineCallAdapter zum Einwickeln von Serverantworten in Coroutines für Asynchronität. Die Basis-URL kann der Einfachheit halber in Konstanten ausgelagert werden. Erstellen Sie dazu das Paket utils innerhalb des dogexplorer-Pakets und dann darin die Datei constants.kt, in die wir die Zeile val baseUrl = “https://api.thedogapi.com/v1/” hinzufügen.
1 2 3 4 5 6 7 8 9 10 |
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .addConverterFactory(GsonConverterFaGsonctory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .baseUrl(baseUrl) .client(okHttpClient) .build() } |
Die dritte Funktion sollte so aussehen:
1 2 3 4 5 |
fun createNetworkApi(retrofit: Retrofit): NetworkApi { return retrofit.create(NetworkApi::class.java) } |
Da wir das Interface noch nicht erstellt haben, fügen wir es dem Projekt hinzu. Erstellen Sie das Paket model und darin NetworkApi. Beachten Sie, dass Sie keine Kotlin-Datei oder -Klasse, sondern ein Interface hinzufügen sollten.
1 2 3 |
interface NetworkApi {} |
Da wir nicht jedes Mal eine neue NetworkApi-Instanz erstellen müssen, wenn wir sie dem ViewModel hinzufügen, verwenden wir die Funktion single, um Singletons zu erstellen. Ändern Sie die Funktion appModule() so, dass sie wie folgt aussieht:
1 2 3 4 5 6 |
val appModule = module { single { createRetrofit(createOkHttpClient()) } single { createNetworkApi(get()) } } |
Damit NetworkApi nützlich wird, definieren wir Methoden, um eine Liste von Hunderassen und Bilder zu erhalten. Bevor wir Methoden schreiben, ist es ratsam, sich mit der Dokumentation unseres Backends vertraut zu machen. Sie finden sie unter https://docs.thedogapi.com/. Daher müssen wir diesen zuerst erhalten, indem wir ein einfaches Formular mit der E-Mail und der Beschreibung unserer Anwendung auf der Seite https://thedogapi.com/signup ausfüllen. Die E-Mail mit dem generierten Schlüssel vom Typ “580e40f4-4144-8d75-a8fb-89a822a3126f” wird an die angegebene E-Mail-Adresse gesendet (Achtung! Dies ist ein ungültiger Schlüssel, nur als Beispiel). Speichern Sie Ihren Schlüssel in der Datei constants.kt als Variable:
1 2 3 |
val xApiKey = "580e40f4-4144-8d75-a8fb-89a822a3126f" |
Die Methode getBreeds() gibt uns eine Liste von Hunderassen zurück. Zuerst müssen wir eine Wrapper-Klasse erstellen, in die die Gson-Bibliothek die Antwort vom Server konvertiert. Sie können diese manuell erstellen, indem Sie eine Klasse erstellen und die entsprechenden Attribute dafür definieren. Eine Liste der erforderlichen Felder finden Sie unter https://docs.thedogapi.com/api-reference/models/breed.
Wir empfehlen jedoch dringend die zweite Methode, die wesentlich produktiver ist.
Fügen Sie das Plugin JSON To Kotlin Class zu Android Studio hinzu. Gehen Sie dazu zum Menü Datei / Einstellungen / Plugins und suchen Sie nach dem gewünschten Namen, installieren Sie das Plugin und starten Sie die IDE neu.
Als nächstes fügen Sie im Package „models“ ein neues Package „entities“ hinzu. Öffnen Sie das Kontextmenü mit einem Rechtsklick und wählen Sie unter den Optionen für das neue Element Kotlin Data Class File from JSON aus. Dieses Plugin ermöglicht es, Kotlin-Datenklassen aus einem JSON-Beispiel zu generieren. Ein Beispielantwort erhalten Sie unter https://docs.thedogapi.com/api-reference/breeds/breeds-list, indem Sie eine Testanfrage senden. Kopieren Sie die erhaltene Antwort in das Eingabefeld des Plugins, geben Sie den Namen der Breed-Klasse an und klicken Sie auf Generieren. Das Plugin erstellt drei Klassen: Breed, Height und Weight. Datenklassen in Kotlin bieten eine bequeme Funktionalität zum Erstellen der sogenannten POJO-Klassen, die nur zur Datenspeicherung dienen. Sie generieren automatisch die Funktionen equals(), hashCode() und copy(), sowie Setter und Getter für jedes Attribut. Der Code ist sehr prägnant, aber effektiv.
Nachdem wir eine Wrapper-Klasse erhalten haben, können wir eine Funktion zum Abrufen einer Liste von Rassen fertigstellen. Es gibt zwei Möglichkeiten für die Implementierung. Die erste besteht darin, den Suspend-Modifikator hinzuzufügen, der das Anhalten der Funktion ermöglicht. Die zweite Option besteht darin, den Aufruf im Deferred-Interface zu umhüllen, welches eine nicht blockierende, stornierbare Zukunft ist. In diesem Fall ist der Aufruf prägnanter. Lassen Sie uns darauf eingehen.
1 2 3 4 |
@GET("breeds") fun getBreeds(@Header("x-api-key") xApiKey: String): Deferred<List> |
Eine Annotation über der Funktion bedeutet, dass unsere Anwendung eine GET-Anfrage an die Endpunkt-Basis-URL + breedsstellt. Der Einfachheit halber übergeben wir den Header als Anforderungsparameter. In komplexeren Anwendungen ist es ratsam, ihn zu einem separaten Auth-Interceptor hinzuzufügen.
Die Funktionalität für die Arbeit mit dem Netzwerk ist nahezu vollständig. Nun wenden wir uns der Implementierung des MVVM-Ansatzes zu. Wenn Sie damit nicht vertraut sind, empfehlen wir den Artikel von Hazem Saleh zu lesen.
Zunächst strukturieren wir unser Projekt. Im dogexplorer-Package erstellen wir das UI-Package, in dem wiederum das main-Package enthalten ist. Wir verschieben MainActivity in dieses Package. Hier erstellen wir eine neue Klasse MainViewModel, die von ViewModel erweitert wird. Für die Arbeit benötigt sie networkApi (um Daten vom Server anzufordern), coroutineScope (um Coroutinen zu verwalten) und LiveData, um Daten an die Aktivität zu übermitteln. Dispatchers.IO im CoroutineScope-Konstruktor bedeutet, dass wir den empfohlenen Thread-Pool für I/O-Operationen mit einem Limit von 64 oder mehr verwenden (wenn das Gerät über mehr als 64 Kerne verfügt).
1 2 3 4 5 6 7 8 9 10 |
class MainViewModel( val networkApi: NetworkApi, val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : ViewModel() { val breeds = MutableLiveData<List>() } |
Fügen Sie eine Funktion hinzu, um eine Liste von Rassen abzurufen. In unserem coroutineScope erstellen wir eine neue Coroutine mit dem Launch-Coroutine-Builder. Hier holen wir eine Liste von Rassen vom Server und posten sie an LiveData.
1 2 3 4 5 6 7 8 |
fun getBreeds() { coroutineScope.launch { val breeds = networkApi.getBreeds(xApiKey).await() breedsLiveData.postValue(breeds) } } |
Wir fügen auch die Stornierung der Coroutine in der onCleared() Funktion unseres MainViewModel hinzu. Beim Zerstören des MainViewModel werden die in seinem CoroutineScope laufenden Coroutinen gestoppt.
1 2 3 4 5 6 |
override fun onCleared() { super.onCleared() coroutineScope.coroutineContext.cancel() } |
Jetzt müssen wir MainViewModel zu MainActivity hinzufügen. Im di-Package erstellen wir eine separate Datei viewModelModule.kt. Wir definieren darin die Variable viewModelModule. Wir könnten sie auch zur Datei appModule.kt hinzufügen, aber mit dem folgenden Beispiel wird es klarer. Diesmal injizieren wir die Abhängigkeit mit einer speziellen viewModel() Methode, die es ermöglicht, ViewModel-Nachfolger in eine Activity oder ein Fragment zu injizieren.
1 2 3 4 5 |
val viewModelModule = module { viewModel { MainViewModel(get()) } } |
Um einen Link zu MainViewModel zu erhalten, müssen wir nur eine Zeile zu MainActivity hinzufügen.
1 2 3 |
val mainViewModel: MainViewModel by viewModel() |
Damit unsere Injektionen funktionieren, müssen wir die Koin-Bibliothek starten. Dies geschieht beim Start der Anwendung. Dazu erstellen wir eine App-Klasse, die von Application erweitert wird, und fügen Code zu ihrer onCreate() Methode hinzu. Vergessen Sie nicht, die neue Klasse im Anwendungsmanifest zu registrieren.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class App : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@App) androidLogger() modules(listOf(appModule, viewModelModule)) } } } |
Der letzte Schliff – wir zeigen die Ergebnisse der Anfrage an den Server auf dem Bildschirm unserer Anwendung an.
Fügen Sie zunächst eine neue Abhängigkeit in build.gradle (App-Modul) hinzu. Synchronisieren Sie Gradle.
1 2 3 |
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04' |
Ändern Sie das Layout des Hauptbildschirms, indem Sie eine RecyclerView hinzufügen.
1 2 3 |
<!--?xml version="1.0" encoding="utf-8"?--> |
Im res/layout-Package fügen Sie das Layout des Listenelements unserer RecyclerView hinzu, das dog_item.xml genannt wird. Es ist so einfach wie möglich und zeigt den Namen der Rasse an.
1 2 3 |
<!--?xml version="1.0" encoding="utf-8"?--> |
Erstellen Sie einen Adapter für RecyclerView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class DogAdapter(val breeds: List) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.dog_item, parent, false) return ViewHolder(view) } override fun getItemCount(): Int { return breeds.size } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.breedName.setText(breeds[position].name) } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { internal var breedName: TextView init { breedName = itemView.findViewById(R.id.dog_name) } } } |
Schließlich können wir die Daten abfragen und im DogAdapter anzeigen. Fügen Sie dazu den Code in der onCreate-Methode der MainActivity hinzu.
1 2 3 4 5 6 7 8 9 |
if (savedInstanceState == null) mainViewModel.getBreeds() mainViewModel.breedsLiveData.observe(this, Observer { recycler.apply { adapter = DogAdapter(it) layoutManager = LinearLayoutManager(this@MainActivity) } }) |
Jetzt können wir unser Projekt starten und voller Stolz strahlen, da wir gerade einen neuen Android-Entwicklungs-Stack gemeistert haben.
Sehen Sie, wie wir eine Lösung für klare Fitness-Logistik mit Kotlin als Kerntechnologie entwickelt haben.
![See how we developed a solution for clear-cut fitness logistics using Kotlin as a core technology](https://redwerk.de/wp-content/uploads/2021/02/screenshot-2.png)