Moderne Android-Entwicklung auf Kotlin. Teil 2

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels " Moderne Android-Entwicklung mit Kotlin (Teil 2) " von Mladen Rakonjac.

Hinweis Dieser Artikel ist eine Übersetzung der Artikelserie von Mladen Rakonjac , Artikel Datum: 23.09.2017. Github Als ich anfing, den ersten Teil von SemperPeritus zu lesen , stellte ich fest , dass der Rest des Teils aus irgendeinem Grund nicht übersetzt wurde. Deshalb mache ich Sie auf den zweiten Teil aufmerksam. Der Artikel erwies sich als umfangreich.

Bild

"Es ist sehr schwierig, ein Projekt zu finden, das alles Neue in der Entwicklung für Android in Android Studio 3.0 abdeckt. Deshalb habe ich beschlossen, es zu schreiben."

In diesem Artikel werden wir Folgendes analysieren:

  1. Android Studio 3 Beta 1 Teil 1
  2. Kotlin Programmiersprache Teil 1
  3. Build-Optionen Teil 1
  4. ConstraintLayout Teil 1
  5. Datenbindungsbibliothek Teil 1
  6. MVVM-Architektur + Repository + Android Manager-Wrapper-Muster
  7. RxJava2 und wie es uns in Teil 3 Architektur hilft
  8. Dolch 2.11, was ist Abhängigkeitsinjektion, warum sollten Sie diesen Teil 4 verwenden
  9. Nachrüstung (mit Rx Java2)
  10. Zimmer (mit Rx Java2)

MVVM-Architektur + Repository + Android Manager-Wrapper-Muster


Ein paar Worte zur Architektur in der Android-Welt


Seit geraumer Zeit verwenden Android-Entwickler keine Architektur in ihren Projekten. In den letzten drei Jahren ist in der Community der Android-Entwickler ein großer Hype um sie aufgetaucht. Die Zeit Gottes ist vorbei und Google hat das Android Architecture Blueprints- Repository mit vielen Beispielen und Anweisungen zu verschiedenen Architekturansätzen veröffentlicht. Schließlich führten sie in Google IO '17 Android Architecture Components ein , eine Sammlung von Bibliotheken, mit denen wir saubereren Code erstellen und Anwendungen verbessern können. Die Komponente sagt, dass Sie alle oder nur einen von ihnen verwenden können. Ich fand sie jedoch alle sehr nützlich. Weiter im Text und in den folgenden Teilen werden wir sie verwenden. Zuerst werde ich auf das Problem im Code eingehen und dann mithilfe dieser Komponenten und Bibliotheken umgestalten, um zu sehen, welche Probleme sie lösen sollen.

Es gibt zwei Hauptarchitekturmuster , die den GUI-Code gemeinsam nutzen:

  • MVP
  • MVVM

Es ist schwer zu sagen, was besser ist. Sie müssen beides versuchen und sich entscheiden. Ich bevorzuge MVVM mit lebenszyklusbewussten Komponenten und werde darüber schreiben. Wenn Sie MVP noch nie ausprobiert haben, gibt es auf Medium unzählige gute Artikel dazu.

Was ist ein MVVM-Muster?


MVVM ist ein Architekturmuster , das als Model-View-ViewModel erweitert wird. Ich denke, dieser Name verwirrt die Entwickler. Wenn ich derjenige wäre, der seinen Namen gefunden hat, würde ich ihn View-ViewModel-Model nennen, da sich das ViewModel in der Mitte befindet und View und Model verbindet .

Ansicht ist eine Abstraktion für Aktivität , Fragment oder eine andere benutzerdefinierte Ansicht (benutzerdefinierte Android-Ansicht ). Bitte beachten Sie, dass es wichtig ist, diese Ansicht nicht mit der Android-Ansicht zu verwechseln. Die Ansicht sollte dumm sein, wir sollten keine Logik darauf schreiben. Die Ansicht sollte keine Daten enthalten. Es sollte einen Verweis auf die ViewModel- Instanz speichern und alle Daten, die die Ansicht benötigt, sollten von dort stammen. Außerdem sollte die Ansicht diese Daten beobachten und das Layout sollte sich ändern, wenn sich die Daten aus dem ViewModel ändern. Zusammenfassend ist View für Folgendes verantwortlich: Layoutansicht für verschiedene Daten und Zustände.

ViewModel ist ein abstrakter Name für eine Klasse, die Daten und Logik enthält, wann diese Daten empfangen werden sollen und wann sie angezeigt werden. ViewModel speichert den aktuellen Status . ViewModel speichert auch einen Link zu einem oder mehreren Modellen und empfängt alle Daten von diesen. Sie sollte zum Beispiel nicht wissen, woher die Daten stammen, aus der Datenbank oder vom Server. Darüber hinaus muss das ViewModel nichts über View wissen. Darüber hinaus sollte ViewModel überhaupt nichts über das Android-Framework wissen.

Modell ist der abstrakte Name für die Ebene, die die Daten für das ViewModel vorbereitet. Dies ist die Klasse, in der wir Daten vom Server empfangen und zwischenspeichern oder in einer lokalen Datenbank speichern. Beachten Sie, dass dies nicht dieselben Klassen sind wie User, Car, Square und andere Modellklassen, in denen einfach Daten gespeichert werden. In der Regel handelt es sich hierbei um eine Implementierung der Repository-Vorlage, die wir später betrachten werden. Das Modell sollte nichts über ViewModel wissen.

MVVM ist bei korrekter Implementierung eine großartige Möglichkeit, Ihren Code zu beschädigen und testbarer zu machen. Dies hilft uns, die SOLID- Prinzipien zu befolgen, sodass unser Code einfacher zu warten ist.

Codebeispiel


Jetzt werde ich ein einfaches Beispiel schreiben, das zeigt, wie das funktioniert.

Erstellen Sie zunächst ein einfaches Modell , das eine Zeile zurückgibt:

RepoModel.kt
class RepoModel { fun refreshData() : String { return "Some new data" } } 


Das Empfangen von Daten ist normalerweise ein asynchroner Aufruf, daher müssen wir darauf warten. Um dies zu simulieren, habe ich die Klasse wie folgt geändert:

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } 


Ich habe die OnDataReadyCallback Schnittstelle mit der onDataReady Methode erstellt. Und jetzt implementiert ( refreshData Methode OnDataReadyCallback . Um das Warten zu simulieren, benutze ich den Handler . Einmal onDataReady 2 Sekunden wird die onDataReady Methode für Klassen aufgerufen, die die OnDataReadyCallback Schnittstelle implementieren.

Erstellen wir ein ViewModel :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false } 


Wie Sie sehen können, gibt es eine Instanz von RepoModel , text , der angezeigt wird, und die Variable isLoading , die den aktuellen Status speichert. Erstellen wir eine refresh , die für das Abrufen von Daten verantwortlich ist:

MainViewModel.kt
 class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } } 


Die refresh ruft refreshData für das RepoModel , das eine OnDataReadyCallback Implementierung in Argumenten verwendet. Ok, aber was ist ein object ? Wann immer Sie eine Schnittstelle implementieren oder eine Extend-Klasse ohne Unterklasse erben möchten, verwenden Sie eine Objektdeklaration . Und wenn Sie dies als anonyme Klasse verwenden möchten? In diesem Fall verwenden Sie den Objektausdruck :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } } 


Wenn wir die refresh aufrufen, müssen wir die Ansicht in den isLoading ändern. Wenn die Daten eintreffen, setzen Sie isLoading auf false .

Wir müssen auch text durch ersetzen
 ObservableField<String> 
und isLoading on
 ObservableField<Boolean> 
. ObservableField ist eine Klasse aus der Datenbindungsbibliothek, die wir verwenden können, anstatt ein Observable-Objekt zu erstellen. Es umschließt das Objekt, das wir beobachten möchten.

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } } 


Beachten Sie, dass ich val anstelle von var verwende , da wir nur den Wert im Feld ändern, nicht aber das Feld selbst. Wenn Sie es initialisieren möchten, verwenden Sie Folgendes:

initobserv.kt
 val text = ObservableField("old data") val isLoading = ObservableField(false) 



Lassen Sie uns unser Layout so ändern, dass es Text und isLoading beobachten kann . Binden Sie zunächst MainViewModel anstelle von Repository :

activity_main.xml
 <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data> 


Dann:

  • Ändern Sie TextView, um Text aus MainViewModel zu beobachten
  • Fügen Sie eine ProgressBar hinzu, die nur sichtbar ist, wenn isLoading true ist
  • Schaltfläche " Hinzufügen", die beim Klicken die Aktualisierungsmethode von MainViewModel aufruft und nur dann anklickbar ist, wenn isLoading false ist

main_activity.xml
 ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ... 


Wenn Sie jetzt ausführen, wird ein View.VISIBLE and View.GONE cannot be used if View is not imported . Nun, lasst uns importieren:

main_activity.xml
 <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> 


Ok, fertig mit dem Layout. Beenden Sie nun mit der Bindung. Wie gesagt, View muss eine Instanz von ViewModel :

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


Endlich können wir rennen


Sie können sehen, dass alte Daten durch neue Daten ersetzt werden .

Dies war ein einfaches MVVM-Beispiel.

Aber es gibt ein Problem, drehen wir den Bildschirm


alte Daten ersetzten neue Daten . Wie ist das möglich? Sehen Sie sich den Aktivitätslebenszyklus an:

Aktivitätslebenszyklus
Bild

Beim onCreate() des Telefons wurde eine neue Instanz von Activity erstellt und die onCreate() -Methode aufgerufen. Schauen Sie sich unsere Aktivitäten an:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


Wie Sie sehen, wurde beim Erstellen einer Aktivitätsinstanz auch eine MainViewModel- Instanz erstellt. Ist es gut, wenn wir irgendwie für jede neu erstellte MainActivity dieselbe Instanz von MainViewModel haben ?

Einführung in lebenszyklusbewusste Komponenten


Weil Viele Entwickler sind mit diesem Problem konfrontiert. Entwickler des Android Framework-Teams haben beschlossen, eine Bibliothek zu erstellen, die zur Lösung dieses Problems beitragen soll. Die ViewModel- Klasse ist eine davon. Dies ist die Klasse, von der alle unsere ViewModel erben sollten.

Lassen Sie uns MainViewModel von ViewModel von lebenszyklusbewussten Komponenten erben. Zuerst müssen wir die Bibliothek für lebenszyklusfähige Komponenten zu unserer build.gradle- Datei hinzufügen :

build.gradle
 dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9" 


Machen Sie MainViewModel zum Erben von ViewModel :

MainViewModel.kt
 package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... } 


Die onCreate () -Methode unserer MainActivity sieht folgendermaßen aus:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } } 


Beachten Sie, dass wir keine neue Instanz von MainViewModel erstellt haben . Wir werden es mit ViewModelProviders bekommen . ViewModelProviders ist eine Utility-Klasse mit einer Methode zum Abrufen von ViewModelProvider . Es geht nur um Umfang . Wenn Sie ViewModelProviders.of (this) in einer Aktivität aufrufen, bleibt Ihr ViewModel so lange aktiv, wie diese Aktivität aktiv ist (bis sie zerstört wird, ohne sie neu zu erstellen). Wenn Sie dies in einem Fragment aufrufen, lebt Ihr ViewModel daher , solange das Fragment lebt usw. Schauen Sie sich das Diagramm an:

Umfang Lebenszyklus
Bild

ViewModelProvider ist dafür verantwortlich, beim ersten Aufruf eine neue Instanz zu erstellen oder die alte zurückzugeben, wenn Ihre Aktivität oder Ihr Fragment neu erstellt wird.

Verwechseln Sie sich nicht mit

 MainViewModel::class.java 

In Kotlin, wenn Sie folgen

 MainViewModel::class 

Dadurch erhalten Sie KClass , das nicht mit Class from Java identisch ist. Wenn wir also .java schreiben, ist es laut Dokumentation:
Gibt eine Instanz der Klasse Java zurück, die dieser Instanz von KClass entspricht
Mal sehen, was passiert, wenn Sie den Bildschirm drehen


Wir haben die gleichen Daten wie vor der Bildschirmdrehung.

Im letzten Artikel habe ich gesagt, dass unsere Anwendung eine Liste der Github-Repositorys erhält und diese anzeigt. Dazu müssen wir die Funktion getRepositories hinzufügen, die eine gefälschte Liste von Repositorys zurückgibt :

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) } 


Wir brauchen auch eine Methode in MainViewModel , die getRepositories von RepoModel aufruft :

MainViewModel.kt
 class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } } 


Und schließlich müssen wir diese Repositorys in RecyclerView anzeigen. Dazu müssen wir:

  • Erstellen Sie das Layout rv_item_repository.xml
  • Fügen Sie RecyclerView zum Layout activity_main.xml hinzu
  • Erstellen Sie RepositoryRecyclerViewAdapter
  • Installieren Sie den Adapter bei recyclerview

Um rv_item_repository.xml zu erstellen , habe ich die CardView-Bibliothek verwendet, daher müssen wir sie zu build.gradle (App) hinzufügen:

 implementation 'com.android.support:cardview-v7:26.0.1' 

So sieht es aus:

rv_item_repository.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout> 


Der nächste Schritt ist das Hinzufügen von RecyclerView zu activity_main.xml . Stellen Sie zuvor sicher, dass Sie die RecyclerView-Bibliothek hinzufügen:

 implementation 'com.android.support:recyclerview-v7:26.0.1' 

activity_main.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout> 



Beachten Sie, dass wir einige TextView-Elemente entfernt haben und die Schaltfläche jetzt loadRepositories anstelle von refresh startet:

button.xml
 <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... /> 


Entfernen wir die Aktualisierungsmethode aus MainViewModel und refreshData aus RepoModel als unnötig.

Jetzt müssen Sie einen Adapter für RecyclerView erstellen:

RepositoryRecyclerViewAdapter.kt
 class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } } 


Beachten Sie, dass ViewHolder anstelle von View eine Instanz vom Typ RvItemRepositoryBinding verwendet, damit wir für jedes Element die Datenbindung im ViewHolder implementieren können. Schämen Sie sich nicht für die einzeilige Funktion (online):

 override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) 

Dies ist nur ein kurzer Eintrag für:

 override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) } 

Und items [position] ist die Implementierung für den Indexoperator. Es ähnelt items.get (Position) .

Eine andere Zeile, die Sie verwirren kann:

 binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) 

Sie können den Parameter durch _ ersetzen, wenn Sie ihn nicht verwenden. Schön, was?

Wir haben den Adapter erstellt, ihn jedoch noch nicht auf die recyclerView in MainActivity angewendet :

MainActivity.kt
 class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


Führen Sie die Anwendung aus


Es ist seltsam. Was ist passiert?

  • Da die Aktivität erstellt wurde, wurde auch ein neuer Adapter mit praktisch leeren Repositorys erstellt
  • Wir drücken den Knopf
  • Ruft loadRepositories auf und zeigt den Fortschritt an
  • Nach 2 Sekunden erhalten wir die Repositorys, der Fortschritt ist ausgeblendet, aber sie werden nicht angezeigt. Dies liegt daran, dass notifyDataSetChanged im Adapter nicht aufgerufen wird
  • Wenn wir den Bildschirm drehen, wird eine neue Aktivität erstellt, sodass ein neuer Adapter mit dem Parameter repositories mit einigen Daten erstellt wird

Können wir notifyDataSetChanged aufrufen, da MainViewModel MainActivity über neue Elemente benachrichtigen soll ?

Wir können nicht.

Dies ist wirklich wichtig, MainViewModel sollte überhaupt nichts über MainActivity wissen.

MainActivity ist derjenige, der über eine Instanz von MainViewModel verfügt. Daher sollte MainActivity auf Änderungen warten und Adapter über die Änderungen informieren.

Aber wie geht das?

Wir können die Repositorys beobachten und nach dem Ändern der Daten unseren Adapter ändern.

Was ist falsch an dieser Entscheidung?

Schauen wir uns den folgenden Fall an:

  • In MainActivity beobachten wir Repositorys: Wenn eine Änderung auftritt, führen wir notifyDataSetChanged aus
  • Wir drücken den Knopf
  • Während wir auf Datenänderungen warten, wird MainActivity möglicherweise aufgrund von Konfigurationsänderungen neu erstellt .
  • Unser MainViewModel lebt noch
  • Nach 2 Sekunden empfängt das Repository- Feld neue Elemente und benachrichtigt den Beobachter, dass sich die Daten geändert haben
  • Der Beobachter versucht, notifyDataSetChanged auf dem Adapter auszuführen, der nicht mehr existiert, weil MainActivity wurde neu erstellt

Nun, unsere Entscheidung ist nicht gut genug.

Einführung in LiveData


LiveData ist eine weitere lebenszyklusbewusste Komponente. Sie basiert auf einem Observable, das den View-Lebenszyklus kennt. Wenn also eine Aktivität aufgrund einer Konfigurationsänderung zerstört wird , weiß LiveData davon und entfernt den Beobachter auch aus der zerstörten Aktivität.

Wir implementieren in MainViewModel :

MainViewModel.kt
 class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


und beginne MainActivity zu beobachten:

MainActivity.kt
 class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


Was bedeutet das Wort? Wenn eine Funktion nur einen Parameter hat, können Sie mit dem Schlüsselwort it auf diesen Parameter zugreifen. Nehmen wir also an, wir haben einen Lambda-Ausdruck, der mit 2 multipliziert werden kann:

 ((a) -> 2 * a) 

Kann wie folgt ersetzt werden:

 (it * 2) 

Wenn Sie die Anwendung jetzt starten, können Sie sicherstellen, dass alles funktioniert


...

Warum bevorzuge ich MVVM gegenüber MVP?



  • Es gibt keine langweilige Oberfläche für View, as ViewModel hat keinen Verweis auf View
  • Es gibt keine langweilige Oberfläche für Presenter, und dies ist nicht erforderlich
  • Konfigurationsänderungen sind viel einfacher zu handhaben
  • Mit MVVM haben wir weniger Code für Aktivitäten, Fragmente usw.

...

Repository-Muster


Schema
Bild

Wie ich bereits sagte, ist Model nur ein abstrakter Name für die Ebene, auf der wir die Daten vorbereiten. Es enthält normalerweise Repositorys und Datenklassen. Jede Entitätsklasse (Datenklasse) verfügt über eine entsprechende Repository- Klasse. Wenn wir beispielsweise die Klassen User und Post haben , müssen wir auch UserRepository und PostRepository haben . Alle Daten stammen von dort. Wir sollten niemals eine Instanz von Shared Preferences oder DB aus View oder ViewModel aufrufen.

So können wir unser RepoModel in GitRepoRepository umbenennen, wobei GitRepo aus dem Github-Repository und Repository aus dem Repository-Muster stammt.

RepoRepositories.kt
 class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Ok, MainViewModel erhält die Github-Liste der Repositorys von GitRepoRepsitories , aber woher bezieht man GitRepoRepositories ?

Sie können den Client oder die Datenbank direkt im Repository von der Instanz aus aufrufen, dies ist jedoch immer noch nicht die beste Vorgehensweise. Ihre Anwendung sollte so modular wie möglich sein. Was ist, wenn Sie verschiedene Clients verwenden, um Volley durch Retrofit zu ersetzen? Wenn Sie eine Art Logik im Inneren haben, wird es schwierig sein, Refactoring durchzuführen. Ihr Repository muss nicht wissen, welchen Client Sie zum Abrufen der Remote-Daten verwenden.

  • Das einzige, was das Repository wissen muss, ist, dass die Daten remote oder lokal ankommen. Es ist nicht erforderlich zu wissen, wie wir diese entfernten oder lokalen Daten erhalten.
  • Das einzige erforderliche Ansichtsmodell sind Daten
  • Die Ansicht sollte nur diese Daten anzeigen.

Als ich gerade mit der Entwicklung auf Android begann, fragte ich mich, wie Anwendungen offline funktionieren und wie die Datensynchronisation funktioniert. Eine gute Anwendungsarchitektur ermöglicht es uns, dies problemlos zu tun. Wenn beispielsweise loadRepositories im ViewModel aufgerufen wird, wenn eine Internetverbindung besteht, kann GitRepoRepositories Daten von einer entfernten Datenquelle empfangen und in einer lokalen Datenquelle speichern. Wenn das Telefon offline ist, kann GitRepoRepository Daten aus dem lokalen Speicher empfangen. Repositorys müssen also Instanzen von RemoteDataSource und LocalDataSource sowie die Logikverarbeitung haben, von der diese Daten stammen sollen.

Fügen Sie eine lokale Datenquelle hinzu :

GitRepoLocalDataSource.kt
 class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: ArrayList<Repository>) } 


Hier haben wir zwei Methoden: die erste, die gefälschte lokale Daten zurückgibt, und die zweite, um fiktive Daten zu speichern.

Fügen Sie eine entfernte Datenquelle hinzu :

GitRepoRemoteDataSource.kt
 class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) } 


Es gibt nur eine Methode, die gefälschte Remote- Daten zurückgibt .

Jetzt können wir unserem Repository eine Logik hinzufügen:

GitRepoRepository.kt
 class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Durch die gemeinsame Nutzung von Quellen können Daten problemlos lokal gespeichert werden.

Was ist, wenn Sie nur Daten aus dem Netzwerk benötigen und dennoch die Repository-Vorlage verwenden müssen? Ja Dies erleichtert das Testen von Code, andere Entwickler können Ihren Code besser verstehen und Sie können ihn schneller unterstützen!

...

Android Manager Wrapper


Was ist, wenn Sie Ihre Internetverbindung in GitRepoRepository überprüfen möchten, um zu erfahren, wo Sie Daten anfordern können? Wir haben bereits gesagt, dass wir keinen Android-bezogenen Code in ViewModel und Model einfügen sollten. Wie soll man mit diesem Problem umgehen?

Schreiben wir einen Wrapper für eine Internetverbindung:

NetManager.kt (Eine ähnliche Lösung gilt für andere Manager, z. B. für NfcManager.)
 class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } } 


Dieser Code funktioniert nur, wenn wir die Berechtigung zum Manifestieren hinzufügen:

 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> 

Aber wie eine Instanz im Repository erstellen, wenn wir den Kontext (nicht über Kontext Die )? Wir können es im Konstruktor anfordern:

GitRepoRepository.kt
 class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Wir haben vor der neuen GitRepoRepository-Instanz im ViewModel erstellt. Wie können wir NetManager jetzt in ViewModel haben, wenn wir Kontext für NetManager benötigen ? Sie können AndroidViewModel aus der Lifecycle-fähigen Komponentenbibliothek verwenden, die einen Kontext hat . Dies ist der Kontext der Anwendung, nicht Aktivität.

MainViewModel.kt
 class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


In dieser Zeile

 constructor(application: Application) : super(application) 

Wir haben einen Konstruktor für MainViewModel definiert . Dies ist erforderlich, da AndroidViewModel eine Instanz der Anwendung in ihrem Konstruktor anfordert . In unserem Konstruktor rufen wir also die Super-Methode auf, die den AndroidViewModel- Konstruktor aufruft , von dem wir erben.

Hinweis: Wir können eine Zeile entfernen, wenn wir Folgendes tun:

 class MainViewModel(application: Application) : AndroidViewModel(application) { ... } 

Und jetzt, da wir die NetManager-Instanz im GitRepoRepository haben , können wir die Internetverbindung überprüfen:

GitRepoRepository.kt
 class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Wenn wir also eine Internetverbindung haben, erhalten wir die gelöschten Daten und speichern sie lokal. Wenn wir keine Internetverbindung haben, erhalten wir lokale Daten.

Hinweis auf Kotlin : Bediener lassen prüft null und gibt den Wert in IT .

In einem der folgenden Artikel werde ich über die Abhängigkeitsinjektion schreiben, wie schlecht es ist, Repository-Instanzen im ViewModel zu erstellen und wie die Verwendung von AndroidViewModel vermieden wird. Ich werde auch über eine große Anzahl von Problemen schreiben, die jetzt in unserem Code enthalten sind. Ich habe sie aus einem Grund verlassen ...

Ich versuche Ihnen die Probleme zu zeigen, damit Sie verstehen, warum all diese Bibliotheken beliebt sind und warum Sie sie verwenden sollten.

PS Ich habe meine Meinung über den Mapper (geändert Mapper ). Ich habe beschlossen, dies in den folgenden Artikeln zu behandeln.

Source: https://habr.com/ru/post/de432826/


All Articles