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.
"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:
- Android Studio 3 Beta 1 Teil 1
- Kotlin Programmiersprache Teil 1
- Build-Optionen Teil 1
- ConstraintLayout Teil 1
- Datenbindungsbibliothek Teil 1
- MVVM-Architektur + Repository + Android Manager-Wrapper-Muster
- RxJava2 und wie es uns in Teil 3 Architektur hilft
- Dolch 2.11, was ist Abhängigkeitsinjektion, warum sollten Sie diesen Teil 4 verwenden
- Nachrüstung (mit Rx Java2)
- 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:
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.ktclass 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:
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:
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")
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")
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
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>){
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.