Hallo Habr!
Wir erinnern Sie daran, dass wir bereits eine 
Vorbestellung für das lang erwartete Buch über die Kotlin-Sprache aus der berühmten Big Nerd Ranch Guides-Reihe haben. Heute haben wir beschlossen, Sie auf eine Übersetzung eines Artikels aufmerksam zu machen, der über Kotlin-Coroutinen und die korrekte Arbeit mit Streams in Android berichtet. Das Thema wird sehr aktiv diskutiert. Der Vollständigkeit halber empfehlen wir Ihnen daher, 
diesen Artikel von Habr und 
diesen ausführlichen Beitrag aus dem Axmor Software-Blog zu lesen.
Das moderne Wettbewerbs-Framework in Java / Android verursacht Rückrufe und führt zu Blockierungszuständen, da Android keine relativ einfache Möglichkeit bietet, die Thread-Sicherheit zu gewährleisten.
Kotlin Coroutinen sind ein sehr effektives und vollständiges Toolkit, das die Verwaltung des Wettbewerbs viel einfacher und produktiver macht.
Pause und Block: Was ist der Unterschied?Coroutinen ersetzen keine Threads, sondern bieten einen Rahmen für deren Verwaltung. Die Philosophie von corutin besteht darin, einen Kontext zu definieren, in dem Sie 
warten können, bis Hintergrundvorgänge abgeschlossen sind, ohne den Hauptthread zu blockieren.
Corutins Ziel ist es in diesem Fall, auf Rückrufe zu verzichten und den Wettbewerb zu vereinfachen.
Einfachstes BeispielNehmen wir zunächst das einfachste Beispiel: Führen Sie coroutine im Kontext von 
Main (Hauptthread) aus. Darin extrahieren wir das Bild aus dem 
IO Stream und senden dieses Bild zur Verarbeitung zurück an 
Main .
 launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }  
Der Code ist als Single-Thread-Funktion einfach. Während 
getImage im zugewiesenen Pool von 
getImage Threads ausgeführt wird, ist der Hauptthread außerdem frei und kann jede andere Aufgabe übernehmen! Die Funktion withContext hält die aktuelle Coroutine an, während ihre Aktion ausgeführt wird ( 
getImage() ). Sobald 
getImage() zurückkehrt und der 
looper aus dem Hauptthread verfügbar wird, nimmt 
imageView.setImageBitmap(image) die Arbeit im Hauptthread wieder auf und ruft 
imageView.setImageBitmap(image) .
Das zweite Beispiel: Jetzt müssen zwei Hintergrundaufgaben erledigt werden, damit sie verwendet werden können. Wir werden das Duett async / await verwenden, damit diese beiden Aufgaben parallel ausgeführt werden, und ihr Ergebnis im Hauptthread verwenden, sobald beide Aufgaben fertig sind:
 val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()  
async ähnelt dem 
launch , gibt jedoch eine 
deferred Rückgabe zurück (eine Kotlin-Entität, die 
Future ), sodass das Ergebnis mit 
await() abgerufen werden kann. Wenn es ohne Parameter aufgerufen wird, funktioniert es im Standardkontext für den aktuellen Bereich.
Auch hier bleibt der Haupt-Thread frei, während wir auf unsere 2 Werte warten.
Wie Sie sehen können, gibt die Startfunktion 
Job , mit dem gewartet werden kann, bis der Vorgang abgeschlossen ist. Dies erfolgt über die Funktion 
join() . Es funktioniert wie in jeder anderen Sprache, mit der Einschränkung, dass es einfach 
die Coroutine suspendiert und den Fluss nicht blockiert .
VersandDispatching ist ein Schlüsselkonzept bei der Arbeit mit Coroutinen. Mit dieser Aktion können Sie von einem Thread zum anderen "springen".
Überlegen Sie, wie das Äquivalent für den Versand in 
Main in Java aussieht.
 runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);  
Die 
Handler für Android ist ein 
Handler basierter Dispatcher. Das ist also in der Tat eine sehr geeignete Implementierung:
 launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }  
launch(Dispatchers.Main) sendet 
Runnable an 
Handler , sodass der Code nicht sofort ausgeführt wird.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) führt seinen Lambda-Ausdruck sofort im aktuellen Thread aus.
Dispatchers.Main stellt sicher, dass Coroutine, wenn die Arbeit wieder aufgenommen wird, an den Haupt-Thread weitergeleitet wird . Darüber hinaus wird Handler hier als native Android-Implementierung zum Senden an die Anwendungsereignisschleife verwendet.
Die genaue Implementierung sieht folgendermaßen aus:
 val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main") 
Hier ist ein guter Artikel, der Ihnen hilft, die Feinheiten des Versands in Android zu verstehen:
Grundlegendes zu Android Core: Looper, Handler und HandlerThread .
Coroutine-KontextDer Coroutine-Kontext (auch als Coroutine-Manager bezeichnet) bestimmt, in welchem Thread sein Code ausgeführt wird, was zu tun ist, wenn eine Ausnahme ausgelöst wird, und verweist auf den übergeordneten Kontext, um die Stornierung zu verbreiten.
 val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... } 
job.cancel() alle Coroutinen ab, deren übergeordnetes 
job.cancel() job . Ein Ausnahmehandler erhält alle Ausnahmen, die in diesen Coroutinen ausgelöst werden.
GeltungsbereichDie 
coroutineScope Schnittstelle vereinfacht die Fehlerbehandlung:
Wenn eine der Tochter-Coroutinen ausfällt, werden auch der gesamte Bereich und alle untergeordneten Coroutinen storniert.
Wenn es im 
async Beispiel nicht möglich war, den Wert zu extrahieren, während eine andere Aufgabe weiter funktionierte, haben wir einen beschädigten Zustand, und damit muss etwas getan werden.
Bei der Arbeit mit 
useValues wird die Funktion 
useValues nur aufgerufen, wenn beide Werte erfolgreich extrahiert wurden. Wenn 
deferred2 fehlschlägt, wird 
deferred1 abgebrochen.
 coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } 
Sie können auch eine ganze Klasse in den Bereich 
CoroutineContext , um einen Standard- 
CoroutineContext dafür 
CoroutineContext und zu verwenden.
Eine Beispielklasse, die die 
CoroutineScope Schnittstelle implementiert:
 open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } } 
Ausführen von Corutin in 
CoroutineScope :
Der Standardstart- oder 
async Manager wird jetzt zum aktuellen Bereichsmanager.
 launch { val foo = withContext(Dispatchers.IO) { … }  
Autonomer Start von Coroutine (außerhalb eines CoroutineScope):
 GlobalScope.launch(Dispatchers.Main) {  
Sie können den Bereich für eine Anwendung sogar definieren, indem Sie den Standard- 
Main festlegen:
 object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate } 
Bemerkungen- Coroutinen beschränken die Interoperabilität mit Java
- Begrenzen Sie die Veränderlichkeit, um Sperren zu vermeiden
- Coroutinen sollen warten, nicht Threads organisieren
- Vermeiden Sie E / A in Dispatchers.Default(undMain...) - dafür ist Dispatchers.IO gedacht
- Streams sind ressourcenintensiv, daher werden Single-Threaded-Kontexte verwendet
- Dispatchers.Defaultbasiert auf- ForkJoinPool, das in Android 5+ eingeführt wurde
- Coroutinen können über Kanäle verwendet werden
Sperren und Rückrufe über Kanäle beseitigenKanaldefinition aus der JetBrains-Dokumentation:
Kanal Channel BlockingQueue konzeptionell sehr ähnlich. Der Hauptunterschied besteht darin, dass der Put-Vorgang nicht blockiert wird, ein Suspend- send (oder ein nicht blockierendes offer ) vorgesehen ist und statt des Take-Vorgangs ein Suspend- receive .
SchauspielerStellen Sie sich ein einfaches Werkzeug für die Arbeit mit Kanälen vor: 
Actor .
Actor ist wiederum 
Handler sehr ähnlich: Wir definieren den Kontext der Coroutine (dh den Thread, in dem wir Aktionen ausführen werden) und arbeiten in einer sequentiellen Reihenfolge damit.
Der Unterschied besteht natürlich darin, dass hier Corutine verwendet werden; 
Sie können die Leistung und die ausgeführte Codepause angeben .
Im Prinzip leitet der 
actor jeden Befehl an den Coroutine-Kanal weiter. Es 
garantiert die Ausführung eines Befehls und schränkt Operationen in seinem Kontext ein . Dieser Ansatz hilft perfekt dabei, 
synchronize Anrufe loszuwerden und alle Threads frei zu halten!
 protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }  
In diesem Beispiel verwenden wir die versiegelten Kotlin-Klassen und wählen aus, welche Aktion ausgeführt werden soll.
 sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update() 
Darüber hinaus werden alle diese Aktionen in die Warteschlange gestellt und niemals parallel ausgeführt. Dies ist ein bequemer Weg, um 
Variabilitätsgrenzen zu erreichen.
Android Lebenszyklus + CoroutinenAkteure können auch sehr nützlich sein, um die Android-Benutzeroberfläche zu steuern, das Abbrechen von Aufgaben zu vereinfachen und eine Überlastung des Hauptthreads zu verhindern.
Lassen Sie uns dies implementieren und 
job.cancel() aufrufen, wenn die Aktivität zerstört wird.
 class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()  
Die 
SupervisorJob Klasse ähnelt dem regulären 
Job mit der einzigen Ausnahme, dass sich die Stornierung nur in der Downstream-Richtung erstreckt.
Daher brechen wir nicht alle Coroutinen in einer 
Activity wenn eine davon fehlschlägt.
Mit 
einer Erweiterungsfunktion , mit der Sie von jeder 
View in 
CoroutineScope aus auf diesen 
CoroutineContext zugreifen können, 
CoroutineContext es etwas besser aus.
 val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext 
Jetzt können wir all dies 
setOnClick Funktion 
setOnClick erstellt einen kombinierten Akteur, um seine 
onClick Aktionen zu steuern. Bei mehreren Abgriffen werden Zwischenaktionen ignoriert, wodurch ANR-Fehler beseitigt werden (die Anwendung reagiert nicht), und diese Aktionen werden im Rahmen der 
Activity . Wenn die Aktivität zerstört wird, wird dies alles abgebrochen.
 fun View.setOnClick(action: suspend () -> Unit) {  
In diesem Beispiel setzen wir den 
Channel auf " 
Conflated , damit einige Ereignisse ignoriert werden, wenn zu viele vorhanden sind. Sie können es durch 
Channel.UNLIMITED ersetzen, wenn Sie Ereignisse lieber in die Warteschlange stellen möchten, ohne eines davon zu verlieren, die Anwendung jedoch vor ANR-Fehlern schützen möchten.
Sie können auch die Coroutinen- und Lifecycle-Frameworks kombinieren, um das Abbrechen von Aufgaben im Zusammenhang mit der Benutzeroberfläche zu automatisieren:
 val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }  
Vereinfachen Sie die Situation mit Rückrufen (Teil 1)Hier erfahren Sie, wie Sie die Verwendung von Callback-basierten APIs mit 
Channel transformieren.
Die API funktioniert folgendermaßen:
- requestBrowsing(url, listener)analysiert den Ordner unter- url.
- Der listenerempfängtonMediaAdded(media: Media)für jede in diesem Ordner gefundene Mediendatei.
- listener.onBrowseEnd()wird beim Parsen des Ordners aufgerufen
Hier ist die alte 
refresh im Inhaltsanbieter für den VLC-Browser:
 private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } } 
Wie kann man es verbessern?Erstellen Sie einen Kanal, der in der 
refresh . Jetzt leiten Browser-Rückrufe die Medien nur noch auf diesen Kanal und schließen ihn dann.
Jetzt ist die 
refresh klarer geworden. Sie erstellt einen Kanal, ruft den VLC-Browser auf, erstellt eine Liste mit Mediendateien und verarbeitet diese.
Anstelle der 
consumeEach oder 
consumeEach Sie 
for das Warten auf das Medium verwenden. Diese Schleife wird 
browserChannel sobald der 
browserChannel geschlossen wird.
 private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)  
Vereinfachung der Situation durch Rückrufe (Teil 2): NachrüstungDer zweite Ansatz: Wir verwenden überhaupt keine Kotlinx-Coroutinen, sondern ein Coroutine-Kern-Framework.
Sehen Sie, wie Coroutinen tatsächlich funktionieren!
Die Funktion 
retrofitSuspendCall umschließt eine 
Retrofit Call Anforderung, um sie zu einer 
suspend Funktion zu machen.
Mit 
suspendCoroutine rufen wir die 
Call.enqueue Methode auf und halten die Coroutine an. Der auf diese Weise bereitgestellte Rückruf ruft 
continuation.resume(response) auf, um die Coroutine mit einer Antwort vom Server fortzusetzen, sobald sie empfangen wird.
Als nächstes müssen wir nur unsere Retrofit-Funktionen in 
retrofitSuspendCall kombinieren, um Abfrageergebnisse mit ihnen zurückzugeben.
 suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }  
Somit erfolgt der Anruf, der das Netzwerk blockiert, im dedizierten Retrofit-Thread, die Coroutine ist hier und wartet auf eine Antwort vom Server, und es gibt keinen Ort, an dem sie in der Anwendung verwendet werden kann!
Diese Implementierung ist von der 
Gildor / Kotlin-Coroutines-Retrofit-Bibliothek inspiriert.
Es gibt auch einen 
JakeWharton / retrofit2-kotlin-coroutines-Adapter mit einer anderen Implementierung, die ein ähnliches Ergebnis 
liefert .
NachwortChannel kann auf viele andere Arten verwendet werden. In 
BroadcastChannel finden Sie leistungsfähigere Implementierungen, die Sie möglicherweise nützlich finden.
Sie können Kanäle auch mit der Funktion 
Produzieren erstellen.
Schließlich ist es unter Verwendung von Kanälen bequem, die Kommunikation zwischen den Komponenten der Benutzeroberfläche zu organisieren: Der Adapter kann Klickereignisse über 
Channel oder beispielsweise über 
Actor an sein Fragment / seine Aktivität übertragen.