Nicht blockierende APIs fordern eine Ausführung an und geben dann die Kontrolle an den aufrufenden Thread zurück, damit dieser vor Abschluss des angeforderten Vorgangs andere Aufgaben ausführen kann. Diese APIs sind nützlich, wenn die angeforderte Arbeit noch nicht abgeschlossen ist oder auf den Abschluss von I/O- oder IPC-Vorgängen, die Verfügbarkeit von stark beanspruchten Systemressourcen oder die Nutzereingabe gewartet werden muss, bevor die Arbeit fortgesetzt werden kann. Besonders gut konzipierte APIs bieten die Möglichkeit, den laufenden Vorgang abzubrechen und die Ausführung von Aufgaben im Namen des ursprünglichen Aufrufers zu beenden. So bleiben Systemintegrität und Akkulaufzeit erhalten, wenn der Vorgang nicht mehr benötigt wird.
Asynchrone APIs sind eine Möglichkeit, ein nicht blockierendes Verhalten zu erreichen. Async-APIs akzeptieren eine Form der Fortsetzung oder einen Callback, der benachrichtigt wird, wenn der Vorgang abgeschlossen ist, oder über andere Ereignisse während des Vorgangs.
Es gibt zwei Hauptgründe für die Erstellung einer asynchronen API:
- Ausführung mehrerer Vorgänge gleichzeitig, wobei der n-te Vorgang gestartet werden muss, bevor der (n-1)-te Vorgang abgeschlossen ist.
- Vermeidung des Blockierens eines aufrufenden Threads, bis ein Vorgang abgeschlossen ist.
Kotlin fördert die strukturierte Parallelität, eine Reihe von Prinzipien und APIs, die auf Suspend-Funktionen basieren und die synchrone und asynchrone Ausführung von Code vom Thread-Blockierungsverhalten entkoppeln. Aussetzungsfunktionen sind nicht blockierend und synchron.
Funktionen pausieren:
- Blockieren Sie den Aufruf-Thread nicht, sondern geben Sie den Ausführungs-Thread als Implementierungsdetail zurück, während Sie auf die Ergebnisse von Vorgängen warten, die an anderer Stelle ausgeführt werden.
- Sie werden synchron ausgeführt und der Aufrufer einer nicht blockierenden API muss nicht gleichzeitig mit der Ausführung nicht blockierender Aufgaben fortfahren, die durch den API-Aufruf initiiert wurden.
Auf dieser Seite finden Sie eine Mindestanforderung an die Erwartungen, die Entwickler bei der Arbeit mit nicht blockierenden und asynchronen APIs haben können. Anschließend folgen eine Reihe von Rezepten zum Erstellen von APIs, die diese Erwartungen in den Programmiersprachen Kotlin oder Java, auf der Android-Plattform oder in den Jetpack-Bibliotheken erfüllen. Wenn Sie sich nicht sicher sind, betrachten Sie die Erwartungen der Entwickler als Anforderungen für jede neue API-Oberfläche.
Erwartungen von Entwicklern an asynchrone APIs
Sofern nicht anders angegeben, beziehen sich die folgenden Erwartungen auf APIs, die nicht ausgesetzt werden.
APIs, die Callbacks akzeptieren, sind in der Regel asynchron.
Wenn eine API einen Rückruf akzeptiert, der nicht dokumentiert ist, dass er nur vor Ort aufgerufen wird (d. h. nur vom aufrufenden Thread aufgerufen wird, bevor der API-Aufruf selbst zurückgegeben wird), wird davon ausgegangen, dass die API asynchron ist und alle anderen in den folgenden Abschnitten beschriebenen Erwartungen erfüllt.
Ein Beispiel für einen Callback, der immer nur vor Ort aufgerufen wird, ist eine höhere Ordnungs-Map- oder Filterfunktion, die vor der Rückgabe einen Mapper oder ein Prädikat auf jedes Element in einer Sammlung anwendet.
Asynchrone APIs sollten so schnell wie möglich eine Antwort zurückgeben.
Entwickler erwarten, dass asynchrone APIs nicht blockierend sind und schnell zurückgegeben werden, nachdem die Anfrage für den Vorgang gestartet wurde. Es sollte immer sicher sein, eine asynchrone API jederzeit aufzurufen. Der Aufruf einer asynchronen API sollte niemals zu ruckeligen Frames oder ANR führen.
Viele Vorgänge und Lebenszyklussignale können von der Plattform oder den Bibliotheken auf Abruf ausgelöst werden. Es ist nicht realistisch, von Entwicklern zu erwarten, dass sie alle potenziellen Aufrufstellen für ihren Code kennen. Beispielsweise kann ein Fragment
in einer synchronen Transaktion als Reaktion auf die View
-Messung und das View
-Layout dem FragmentManager
hinzugefügt werden, wenn App-Inhalte eingefügt werden müssen, um den verfügbaren Bereich zu füllen (z. B. RecyclerView
). Ein LifecycleObserver
, das auf den onStart
-Lebenszyklus-Callback dieses Fragments reagiert, kann hier einmalig Startvorgänge ausführen. Dies kann sich auf einen kritischen Codepfad auswirken, um einen ruckelfreien Animationsframe zu erstellen. Entwickler sollten sich immer darauf verlassen können, dass der Aufruf einer beliebigen asynchronen API als Reaktion auf diese Art von Lebenszyklus-Callbacks nicht zu einem ruckeligen Frame führt.
Das bedeutet, dass die Arbeit, die von einer asynchronen API vor der Rückgabe ausgeführt wird, sehr gering sein muss. Es reicht aus, einen Eintrag für die Anfrage und den zugehörigen Callback zu erstellen und bei der Ausführungs-Engine zu registrieren, die die Arbeit ausführt. Wenn für die Registrierung für einen asynchronen Vorgang IPC erforderlich ist, sollten bei der Implementierung der API alle erforderlichen Maßnahmen ergriffen werden, um diese Entwicklererwartung zu erfüllen. Dazu gehören:
- Untergeordnete IPC als One-Way-Binder-Aufruf implementieren
- Einen bidirektionalen Binderaufruf an den Systemserver ausführen, bei dem für die Registrierung keine stark umkämpfte Sperre erforderlich ist
- Die Anfrage an einen Worker-Thread im App-Prozess posten, um eine blockierende Registrierung über IPC auszuführen
Asynchrone APIs sollten „void“ zurückgeben und nur bei ungültigen Argumenten eine Ausnahme auslösen.
Asynchrone APIs sollten alle Ergebnisse des angeforderten Vorgangs an den angegebenen Rückruf senden. So kann der Entwickler einen einzigen Codepfad für den Erfolg und die Fehlerbehandlung implementieren.
Bei asynchronen APIs können Argumente auf „null“ geprüft und NullPointerException
geworfen werden oder geprüft werden, ob die angegebenen Argumente in einem gültigen Bereich liegen, und IllegalArgumentException
geworfen werden. Bei einer Funktion, die beispielsweise einen float
im Bereich von 0
bis 1f
akzeptiert, kann die Funktion prüfen, ob der Parameter in diesem Bereich liegt, und IllegalArgumentException
auswerfen, wenn er nicht in diesem Bereich liegt. Oder eine kurze String
kann auf Einhaltung eines gültigen Formats wie nur alphanumerischen Zeichen geprüft werden. Denken Sie daran, dass der Systemserver dem App-Prozess niemals vertrauen darf. Jeder Systemdienst sollte diese Prüfungen im Systemdienst selbst duplizieren.)
Alle anderen Fehler sollten über den angegebenen Rückruf gemeldet werden. Dazu zählen unter anderem:
- Endgültiger Fehler beim angeforderten Vorgang
- Sicherheitsausnahmen für fehlende Autorisierungen oder Berechtigungen, die zum Ausführen des Vorgangs erforderlich sind
- Kontingent für die Ausführung des Vorgangs überschritten
- Der App-Prozess ist nicht ausreichend im Vordergrund, um den Vorgang auszuführen
- Erforderliche Hardware getrennt
- Netzwerkfehler
- Zeitüberschreitungen
- Binder-Endung oder nicht verfügbarer Remote-Prozess
Asynchrone APIs sollten einen Mechanismus zum Abbrechen bieten
Async-APIs sollten eine Möglichkeit bieten, einem laufenden Vorgang anzugeben, dass der Aufrufer kein Interesse mehr am Ergebnis hat. Dieser Vorgang sollte zwei Dinge signalisieren:
Hard References zu vom Aufrufer bereitgestellten Callbacks sollten freigegeben werden.
Callbacks, die für asynchrone APIs bereitgestellt werden, können harte Verweise auf große Objektgraphen enthalten. Laufende Arbeiten, die einen harten Verweis auf diesen Callback enthalten, können verhindern, dass diese Objektgraphen vom Garbage Collector erfasst werden. Wenn diese Rückrufreferenzen beim Abbruch freigegeben werden, können diese Objektgraphen viel früher zur Garbage Collection freigegeben werden, als wenn die Arbeit zu Ende geführt werden würde.
Die Ausführungs-Engine, die die Arbeit für den Aufrufer ausführt, kann diese Arbeit beenden.
Von asynchronen API-Aufrufen initiierte Aufgaben können einen hohen Stromverbrauch oder andere Systemressourcen beanspruchen. APIs, mit denen Aufrufer signalisieren können, dass diese Arbeit nicht mehr erforderlich ist, ermöglichen es, diese Arbeit zu beenden, bevor weitere Systemressourcen verbraucht werden.
Besondere Überlegungen für im Cache gespeicherte oder eingefrorene Apps
Beachten Sie beim Entwerfen asynchroner APIs, bei denen Callbacks aus einem Systemprozess stammen und an Apps gesendet werden, Folgendes:
- Prozesse und App-Lebenszyklus: Der Prozess der Empfänger-App befindet sich möglicherweise im Cache-Status.
- Cache-Apps eingefroren: Möglicherweise ist der Prozess der Empfänger-App eingefroren.
Wenn ein App-Prozess den Cache-Status erreicht, werden keine nutzersichtbaren Komponenten wie Aktivitäten und Dienste aktiv gehostet. Die App wird im Arbeitsspeicher gehalten, falls sie wieder für Nutzer sichtbar wird, sollte aber in der Zwischenzeit keine Arbeit ausführen. In den meisten Fällen sollten Sie das Senden von App-Callbacks pausieren, wenn sich die App im Cache-Status befindet, und fortsetzen, wenn die App den Cache-Status verlässt, damit keine Arbeit in gecachten App-Prozessen ausgeführt wird.
Eine im Cache gespeicherte App kann auch eingefroren sein. Wenn eine App eingefroren ist, erhält sie keine CPU-Zeit und kann keine Arbeit ausführen. Alle Aufrufe der registrierten Callbacks dieser App werden zwischengespeichert und ausgeführt, sobald die App wieder freigegeben wird.
Gepufferte Transaktionen an App-Callbacks sind möglicherweise veraltet, wenn die App wieder aktiviert und die Transaktionen verarbeitet werden. Der Puffer ist endlich und ein Überlauf würde zum Absturz der Empfänger-App führen. Um zu verhindern, dass Apps mit veralteten Ereignissen überlastet oder ihre Puffer überlaufen, sollten Sie keine App-Callbacks senden, während ihr Prozess eingefroren ist.
Wird überprüft:
- Sie sollten in Betracht ziehen, das Senden von App-Callbacks anzuhalten, während der Prozess der App im Cache gespeichert wird.
- Sie MÜSSEN das Senden von App-Callbacks pausieren, während der Prozess der App eingefroren ist.
Zustands-Tracking
So erfassen Sie, wann Apps den Cache-Status erreichen oder verlassen:
mActivityManager.addOnUidImportanceListener(
new UidImportanceListener() { ... },
IMPORTANCE_CACHED);
So können Sie nachverfolgen, wann Apps eingefroren oder entsperrt werden:
IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);
Strategien zum Fortsetzen des Sendens von App-Callbacks
Unabhängig davon, ob Sie das Senden von App-Callbacks pausieren, wenn die App den Cache-Status oder den eingefrorenen Status betritt, sollten Sie das Senden der registrierten Callbacks der App fortsetzen, sobald die App den jeweiligen Status verlässt, bis die App ihren Callback deregistriert hat oder der App-Prozess beendet wird.
Beispiel:
IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
shouldSendCallbacks = false;
} else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
shouldSendCallbacks = true;
}
});
Alternativ können Sie RemoteCallbackList
verwenden, damit keine Rückrufe an den Zielprozess gesendet werden, wenn er eingefroren ist.
Beispiel:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));
callback.foo()
wird nur aufgerufen, wenn der Prozess nicht eingefroren ist.
Apps speichern häufig Updates, die sie über Callbacks erhalten haben, als Snapshot des aktuellen Status. Angenommen, es gibt eine hypothetische API für Apps, mit der der verbleibende Akkustand in Prozent überwacht werden kann:
interface BatteryListener {
void onBatteryPercentageChanged(int newPercentage);
}
Stellen Sie sich vor, dass mehrere Statusänderungsereignisse auftreten, wenn eine App eingefroren ist. Wenn die App entsperrt wird, sollten Sie nur den aktuellen Status an die App senden und andere veraltete Statusänderungen löschen. Diese Übermittlung sollte sofort nach dem Aufheben der Sperre erfolgen, damit die App auf dem neuesten Stand bleibt. So gehts:
RemoteCallbackList<IInterface> rc =
new RemoteCallbackList.Builder<IInterface>(
RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
.setExecutor(executor)
.build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));
In einigen Fällen können Sie den letzten an die App gesendeten Wert erfassen, damit die App nicht über denselben Wert benachrichtigt werden muss, sobald die Sperrung aufgehoben wird.
Der Status kann auch als komplexere Daten ausgedrückt werden. Angenommen, es gibt eine hypothetische API, über die Apps über Netzwerkschnittstellen benachrichtigt werden:
interface NetworkListener {
void onAvailable(Network network);
void onLost(Network network);
void onChanged(Network network);
}
Wenn Sie Benachrichtigungen für eine App pausieren, sollten Sie sich merken, welche Netzwerke und Bundesländer die App zuletzt gesehen hat. Nach der Wiederaufnahme wird empfohlen, die App in dieser Reihenfolge über alte Netzwerke, die verloren gegangen sind, neue Netzwerke, die verfügbar geworden sind, und bestehende Netzwerke, deren Status sich geändert hat, zu informieren.
Die App wird nicht über Netzwerke benachrichtigt, die verfügbar gemacht wurden und dann verloren gingen, während Callbacks pausiert waren. Apps sollten keine vollständige Aufzeichnung der Ereignisse erhalten, die aufgetreten sind, während sie eingefroren waren. Die API-Dokumentation sollte nicht versprechen, Ereignisstreams außerhalb expliziter Lebenszyklusstatus unterbrechungsfrei zu liefern. Wenn die App in diesem Beispiel die Netzwerkverfügbarkeit kontinuierlich überwachen muss, muss sie sich in einem Lebenszyklusstatus befinden, der verhindert, dass sie im Cache gespeichert oder eingefroren wird.
Zusammenfassend sollten Sie Ereignisse, die nach der Pausierung und vor der Wiederaufnahme von Benachrichtigungen aufgetreten sind, zusammenführen und den aktuellen Status an die registrierten App-Callbacks senden.
Überlegungen zur Entwicklerdokumentation
Die Übermittlung von asynchronen Ereignissen kann sich verzögern, entweder weil der Absender die Übermittlung für einen bestimmten Zeitraum pausiert hat (wie im vorherigen Abschnitt beschrieben) oder weil die Empfänger-App nicht genügend Geräteressourcen erhalten hat, um das Ereignis zeitnah zu verarbeiten.
Entwickler sollten nicht davon ausgehen, dass die Zeit zwischen der Benachrichtigung ihrer App über ein Ereignis und dem tatsächlichen Eintreten des Ereignisses gleich ist.
Erwartungen von Entwicklern bei der Sperrung von APIs
Entwickler, die mit der strukturierten Parallelität von Kotlin vertraut sind, erwarten von jeder API, die die Ausführung anhält, das folgende Verhalten:
Bei Suspend-Funktionen sollte die gesamte zugehörige Arbeit abgeschlossen sein, bevor eine Rückgabe oder ein Fehler geworfen wird.
Die Ergebnisse nicht blockierender Vorgänge werden als normale Funktionsrückgabewerte zurückgegeben und Fehler werden durch das Auslösen von Ausnahmen gemeldet. Das bedeutet oft, dass Rückrufparameter nicht erforderlich sind.
Suspend-Funktionen sollten Callback-Parameter nur vor Ort aufrufen
Bei einer Suspend-Funktion sollte immer alle zugehörigen Aufgaben abgeschlossen sein, bevor die Funktion zurückkehrt. Sie darf also nie einen bereitgestellten Callback oder anderen Funktionsparameter aufrufen oder einen Verweis darauf behalten, nachdem die Suspend-Funktion zurückgegeben wurde.
Ausgesetzte Funktionen, die Callback-Parameter akzeptieren, sollten den Kontext beibehalten, sofern nicht anders dokumentiert.
Wenn eine Funktion in einer suspend-Funktion aufgerufen wird, wird sie im CoroutineContext
des Aufrufers ausgeführt. Da Suspend-Funktionen alle zugehörigen Aufgaben vor dem Zurückgeben oder Auslösen abschließen und nur Callback-Parameter vor Ort aufrufen sollten, wird standardmäßig davon ausgegangen, dass alle solchen Callbacks auch mit dem zugehörigen Dispatcher auf der aufrufenden CoroutineContext
ausgeführt werden. Wenn die API dazu dient, einen Callback außerhalb der aufrufenden CoroutineContext
auszuführen, sollte dieses Verhalten klar dokumentiert werden.
Die Funktion „Suspend“ sollte die Abbruchsfunktion von kotlinx.coroutines unterstützen
Alle angebotenen Funktionen zum Pausieren sollten mit der Job-Stornierung gemäß kotlinx.coroutines
zusammenarbeiten. Wenn der aufrufende Job eines laufenden Vorgangs abgebrochen wird, sollte die Funktion so schnell wie möglich mit einer CancellationException
fortgesetzt werden, damit der Aufrufer so schnell wie möglich die Aufräumarbeiten durchführen und fortfahren kann. Das wird automatisch von suspendCancellableCoroutine
und anderen von kotlinx.coroutines
angebotenen APIs zur Sperrung verwaltet. Bibliotheken sollten suspendCoroutine
in der Regel nicht direkt verwenden, da dieses Verhalten standardmäßig nicht unterstützt wird.
Bei Funktionen, die blockierende Aufgaben im Hintergrund ausführen (nicht im Haupt- oder UI-Thread), muss eine Möglichkeit zur Konfiguration des verwendeten Dispatchers vorhanden sein.
Es wird nicht empfohlen, eine blockierende Funktion vollständig anzuhalten, um Threads zu wechseln.
Der Aufruf einer Suspend-Funktion darf nicht zum Erstellen zusätzlicher Threads führen, ohne dass der Entwickler seinen eigenen Thread oder Threadpool für die Ausführung dieser Arbeit bereitstellen kann. Ein Konstruktor kann beispielsweise eine CoroutineContext
akzeptieren, die für die Hintergrundarbeit der Methoden der Klasse verwendet wird.
Bei Suspend-Funktionen, die einen optionalen CoroutineContext
- oder Dispatcher
-Parameter akzeptieren, um zu diesem Dispatcher zu wechseln und blockierende Arbeit auszuführen, sollte stattdessen die zugrunde liegende blockierende Funktion freigegeben und den aufrufenden Entwicklern empfohlen werden, mit ihrem eigenen Aufruf von „withContext“ die Arbeit an einen ausgewählten Dispatcher weiterzuleiten.
Klassen, die coroutines starten
Klassen, die Tasks starten, müssen eine CoroutineScope
haben, um diese Startvorgänge auszuführen. Die Einhaltung der Prinzipien für strukturierte Parallelität impliziert die folgenden strukturellen Muster für die Erlangung und Verwaltung dieses Umfangs.
Bevor Sie eine Klasse schreiben, die parallele Aufgaben in einem anderen Gültigkeitsbereich startet, sollten Sie alternative Muster in Betracht ziehen:
class MyClass {
private val requests = Channel<MyRequest>(Channel.UNLIMITED)
suspend fun handleRequests() {
coroutineScope {
for (request in requests) {
// Allow requests to be processed concurrently;
// alternatively, omit the [launch] and outer [coroutineScope]
// to process requests serially
launch {
processRequest(request)
}
}
}
}
fun submitRequest(request: MyRequest) {
requests.trySend(request).getOrThrow()
}
}
Wenn eine suspend fun
für die Ausführung paralleler Aufgaben freigegeben wird, kann der Aufrufer den Vorgang in seinem eigenen Kontext aufrufen. Es ist dann nicht mehr erforderlich, dass MyClass
eine CoroutineScope
verwaltet. Die Serialisierung der Verarbeitung von Anfragen wird einfacher und der Status kann oft als lokale Variablen von handleRequests
anstelle von Klasseneigenschaften vorliegen, die andernfalls eine zusätzliche Synchronisierung erfordern würden.
Klassen, die Tasks verwalten, sollten die Methoden „close“ und „cancel“ bereitstellen.
Klassen, die coroutines als Implementierungsdetails starten, müssen eine Möglichkeit bieten, diese laufenden parallelen Aufgaben sauber herunterzufahren, damit sie nicht unkontrollierte parallele Arbeit in einen übergeordneten Bereich lecken. In der Regel geschieht dies durch Erstellen einer untergeordneten Job
einer bereitgestellten CoroutineContext
:
private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)
fun cancel() {
myJob.cancel()
}
Es kann auch eine join()
-Methode bereitgestellt werden, mit der der Nutzercode den Abschluss aller ausstehenden parallelen Arbeiten abwarten kann, die vom Objekt ausgeführt werden.
Dies kann auch die Bereinigung umfassen, die durch das Abbrechen eines Vorgangs ausgeführt wird.
suspend fun join() {
myJob.join()
}
Benennung von Terminalvorgängen
Der Name für Methoden, mit denen laufende, parallele Aufgaben, die einem Objekt zugewiesen sind, sauber beendet werden, sollte den Verhaltensvertrag für das Herunterfahren widerspiegeln:
Verwenden Sie close()
, wenn laufende Vorgänge abgeschlossen werden können, aber nach dem Rückgabewert des close()
-Aufrufs keine neuen Vorgänge gestartet werden dürfen.
Verwenden Sie cancel()
, wenn laufende Vorgänge vor dem Abschluss abgebrochen werden können.
Nach dem Rückgabewert des Aufrufs von cancel()
dürfen keine neuen Vorgänge gestartet werden.
Klassenkonstruktoren akzeptieren CoroutineContext, nicht CoroutineScope
Wenn Objekte nicht direkt in einem bereitgestellten übergeordneten Bereich gestartet werden dürfen, ist CoroutineScope
als Konstruktorparameter nicht geeignet:
// Don't do this
class MyClass(scope: CoroutineScope) {
private val myJob = Job(parent = scope.`CoroutineContext`[Job])
private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)
// ... the [scope] constructor parameter is never used again
}
CoroutineScope
wird zu einem unnötigen und irreführenden Wrapper, der in einigen Anwendungsfällen nur zum Übergeben als Konstruktorparameter erstellt und dann verworfen wird:
// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))
CoroutineContext-Parameter haben standardmäßig den Wert „EmptyCoroutineContext“.
Wenn ein optionaler CoroutineContext
-Parameter in einer API-Oberfläche angezeigt wird, muss der Standardwert der Empty`CoroutineContext`
-Sentinel sein. Dies ermöglicht eine bessere Zusammensetzung des API-Verhaltens, da ein Empty`CoroutineContext`
-Wert von einem Aufrufer genauso behandelt wird wie das Akzeptieren des Standardwerts:
class MyOuterClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val innerObject = MyInnerClass(`CoroutineContext`)
// ...
}
class MyInnerClass(
`CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
private val job = Job(parent = `CoroutineContext`[Job])
private val scope = CoroutineScope(`CoroutineContext` + job)
// ...
}