Κορεντίνο Coroutine Best Practices

Είναι ένα συνεχώς διατηρημένο σύνολο βέλτιστων πρακτικών για τη χρήση της Kotlin Coroutines στο Android. Σχολιάστε παρακάτω εάν έχετε οποιεσδήποτε προτάσεις σχετικά με οτιδήποτε πρέπει να προστεθεί.

  1. Χειρισμός κύκλων ζωής Android

Με παρόμοιο τρόπο που χρησιμοποιείτε CompositeDisposables με το RxJava, οι Kotlin Coroutines πρέπει να ακυρωθούν την κατάλληλη στιγμή με την επίγνωση του Android Livecycles με Δραστηριότητες και Θραύσματα.

α) Χρήση μοντέλων προβολής Android

Αυτός είναι ο ευκολότερος τρόπος για να ρυθμίσετε τις κορουτίνες ώστε να κλείνουν την κατάλληλη στιγμή, αλλά λειτουργούν μόνο μέσα σε ένα Android ViewModel που διαθέτει μια λειτουργία onCleared, ώστε οι εργασίες coroutine να μπορούν να ακυρωθούν αξιόπιστα από:

ιδιωτική προβολή valModelJob = εργασία ()
ιδιωτικό κύμα uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
αντικατάσταση διασκέδασης onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}}

Σημείωση: Από το ViewModels 2.1.0-alpha01, αυτό δεν είναι πλέον απαραίτητο. Δεν χρειάζεται πλέον να έχετε το μοτίβο προβολής σας να εφαρμόσει το CoroutineScope, onCleared ή να προσθέσει μια εργασία. Απλά χρησιμοποιήστε το "viewModelscope.launch {}". Σημειώστε ότι το 2.x σημαίνει ότι η εφαρμογή σας θα πρέπει να βρίσκεται στο AndroidX, επειδή δεν είμαι σίγουρος ότι σχεδιάζουν να το παραδώσουν στην έκδοση 1.x του ViewModels.

β) Χρήση παρατηρητών κύκλου ζωής

Αυτή η άλλη τεχνική δημιουργεί ένα πεδίο που συνδέεστε με μια δραστηριότητα ή ένα κομμάτι (ή οτιδήποτε άλλο υλοποιεί έναν Κύκλο ζωής του Android):

/ **
 * Περιεχόμενο Coroutine που ακυρώνεται αυτόματα όταν το UI καταστρέφεται
 * /
class UiLifecycleScope: CoroutineScope, LifecycleObserver {

    ιδιωτική τελευταία εργασία: δουλειά
    παράκαμψη της λέξης coroutineContext: CoroutineContext
        get () = εργασία + Dispatchers.Main

    @OnLifecycleEvent (Lifecycle.Event.ON_START)
    fun onCreate () {)
        εργασία = εργασία ()
    }}

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    fun destroy () = job.cancel ()
}}
... μέσα στη Δραστηριότητα υποστήριξης Lib ή Fragment
ιδιωτικό κύμα uiScope = UiLifecycleScope ()
αντικατάσταση διασκέδασης onCreate (savedInstanceState: δέσμη) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}}

γ) GlobalScope

Αν χρησιμοποιείτε το GlobalScope, είναι ένα πεδίο που διαρκεί τη διάρκεια ζωής της εφαρμογής. Θα χρησιμοποιούσατε αυτό για συγχρονισμό φόντου, ανανέωση repo κ.λπ. (δεν συνδέεται με κύκλο ζωής δραστηριότητας).

δ) Υπηρεσίες

Οι υπηρεσίες μπορούν να ακυρώσουν τις εργασίες τους στο onDestroy:

ιδιωτική υπηρεσία υπηρεσίας val = Job ()
ιδιωτική υπηρεσία κύματοςScope = CoroutineScope (Dispatchers.Main + υπηρεσίαJob)
αντικατάσταση διασκέδασης onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}}

2. Διαχείριση εξαιρέσεων

α) Σε ασύγχρονο εναντίον εκτόξευσης εναντίον runBlocking

Είναι σημαντικό να σημειωθεί ότι οι εξαιρέσεις σε ένα μπλοκ εκτόξευσης {} θα καταρρεύσουν την εφαρμογή χωρίς χειριστή εξαιρέσεων. Να ρυθμίζετε πάντα έναν προεπιλεγμένο χειριστή εξαιρέσεων για να περάσετε ως παράμετρος για την εκκίνηση.

Μια εξαίρεση μέσα σε ένα μπλοκ runBlocking {} θα καταρρεύσει την εφαρμογή, εκτός αν προσθέσετε ένα try catch. Προσθέστε πάντα ένα try / catch εάν χρησιμοποιείτε το runBlocking. Στην ιδανική περίπτωση, χρησιμοποιήστε μόνο RunBlocking για δοκιμές μονάδας.

Μια εξαίρεση που ρίχνεται μέσα σε ένα μπλοκ async {} δεν θα διαδοθεί ή θα εκτελεστεί μέχρι να αναμένεται το μπλοκ επειδή είναι πραγματικά μια Java Deferred από κάτω. Η λειτουργία / μέθοδος κλήσης πρέπει να καλύπτει εξαιρέσεις.

β) Απαγόρευση εξαιρέσεων

Εάν χρησιμοποιείτε async για να εκτελέσετε κώδικα που μπορεί να προκαλέσει εξαιρέσεις, πρέπει να τυλίξετε τον κώδικα σε ένα coroutineScope για να καλύψετε τις εξαιρέσεις σωστά (χάρη στον LouisC για το παράδειγμα):

προσπαθήστε {
    coroutineScope {
        val mayFailAsync1 = async {
            mayFail1 ()
        }}
        val mayFailAsync2 = async {
            mayFail2 ()
        }}
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }}
} αλίευση (e: IOException) {
    // χειρισου το
    throw MyIoException ("Σφάλμα στο IO", e)
} αλίευση (e: AnotherException) {
    // χειριστείτε και αυτό
    throw MyOtherException ("Σφάλμα κάνοντας κάτι", e)
}}

Όταν πιάσετε την εξαίρεση, τυλίξτε την σε άλλη Εξαίρεση (παρόμοια με αυτή που κάνετε για το RxJava), ώστε να αποκτήσετε τη γραμμή stacktrace στον δικό σας κώδικα αντί να δείτε ένα stacktrace με μόνο κώδικα coroutine.

γ) Εξαιρέσεις καταγραφής

Εάν χρησιμοποιείτε το GlobalScope.launch ή έναν ηθοποιό, πάνε πάντοτε σε έναν διαχειριστή εξαιρέσεων που μπορεί να καταγράψει εξαιρέσεις. Π.χ.

val errorHandler = CoroutineExceptionHandler {_, εξαίρεση ->
  // συνδεθείτε στο Crashlytics, logcat, κλπ.
}}
val δουλειά = GlobalScope.launch (errorHandler) {
...
}}

Σχεδόν πάντοτε, θα πρέπει να έχετε δομημένα πεδία στο Android και θα πρέπει να χρησιμοποιείτε χειριστή:

val errorHandler = CoroutineExceptionHandler {_, εξαίρεση ->
  // log σε Crashlytics, logcat, κλπ.? μπορεί να εγχυθεί εξάρτηση
}}
val supervisor = SupervisorJob () // ακυρώθηκε με τη διάρκεια κύκλου ζωής δραστηριότητας
με (CoroutineScope (coroutineContext + επόπτης)) {
  val κάτι = εκκίνηση (errorHandler) {
    ...
  }}
}}

Και αν χρησιμοποιείτε async και περιμένετε, πάντα τυλίξτε το try / catch όπως περιγράψαμε παραπάνω, αλλά συνδεθείτε όπως είναι απαραίτητο.

δ) Εξετάστε το αποτέλεσμα / Σφάλμα σφραγισμένη κλάση

Εξετάστε τη χρήση μιας σφραγισμένης κλάσης αποτελεσμάτων που μπορεί να συγκρατήσει ένα σφάλμα αντί να ρίξει εξαιρέσεις:

σφραγισμένη κλάση Αποτέλεσμα  {
  τάξη δεδομένων Επιτυχία (δεδομένα val: T): Αποτέλεσμα ()
  τάξη δεδομένων Σφάλμα (σφάλμα κύματος: E): Αποτέλεσμα ()
}}

ε) Το όνομα Coroutine Context

Όταν δηλώνετε ένα async λάμβδα, μπορείτε επίσης να το ονομάσετε έτσι:

async (CoroutineName ("MyCoroutine")) {}

Εάν δημιουργείτε το δικό σας νήμα για να εκτελέσετε, μπορείτε επίσης να το ονομάσετε κατά τη δημιουργία αυτού του εκτελέσιμου κλωστή:

newSingleThreadContext ("MyCoroutineThread")

3. Πισίνες Executor και προεπιλεγμένα μεγέθη πισίνα

Το Coroutines είναι πραγματικά πολύ συνεργατικό multitasking (με βοήθεια μεταγλωττιστή) σε περιορισμένο μέγεθος πισίνας νήματος. Αυτό σημαίνει ότι αν κάνετε κάτι μπλοκάρισμα στο coroutine (π.χ., χρησιμοποιήστε ένα API αποκλεισμού), θα δεσμεύσετε ολόκληρο το νήμα μέχρι να ολοκληρωθεί η λειτουργία αποκλεισμού. Το coroutine επίσης δεν θα ανασταλεί αν δεν κάνετε απόδοση ή καθυστέρηση, οπότε αν έχετε μεγάλο βρόχο επεξεργασίας, φροντίστε να ελέγξετε αν έχει ακυρωθεί η coroutine (καλέστε "ensureActive ()" στο πεδίο εφαρμογής), ώστε να μπορείτε να απελευθερώσετε το νήμα? αυτό είναι παρόμοιο με το πώς λειτουργεί το RxJava.

Οι κορθίνες της Kotlin έχουν μερικές χτισμένες στους αποστολείς (ισοδύναμες με τους προγραμματιστές στο RxJava). Ο κύριος αποστολέας (εάν δεν καθορίζετε τίποτα για να τρέξετε) είναι ο UI. θα πρέπει να αλλάξετε μόνο τα στοιχεία UI σε αυτό το πλαίσιο. Υπάρχει επίσης ένας Dispatchers.Unconfined ο οποίος μπορεί να hop μεταξύ UI και τα νήματα φόντου έτσι δεν είναι σε ένα μόνο νήμα? αυτό γενικά δεν θα πρέπει να χρησιμοποιείται εκτός από τις δοκιμές μονάδας. Υπάρχει Dispatchers.IO για χειρισμό IO (κλήσεις δικτύου που αναστέλλουν συχνά). Τέλος, υπάρχει ένας Dispatchers.Default ο οποίος είναι ο κύριος πυρήνας υποστρώματος υποβάθρου, αλλά αυτό περιορίζεται στον αριθμό CPU.

Στην πράξη, θα πρέπει να χρησιμοποιήσετε μια διεπαφή για τους κοινούς αποστολείς που μεταβιβάζονται μέσα από τον κατασκευαστή κλάσης σας έτσι ώστε να μπορείτε να ανταλλάξετε διαφορετικές για δοκιμή. Π.χ.:

διεπαφή CoroutineDispatchers {
  παράμετρος UI: Αποστολέας
  κύρος IO: Αποστολέας
  Υπολογισμός: Αποστολέας
  fun newThread (όνομα κύματος: συμβολοσειρά): Αποστολέας
}}

4. Αποφυγή της Διαφθοράς Δεδομένων

Δεν έχουν λειτουργίες αναστολής να τροποποιούν δεδομένα εκτός της λειτουργίας. Για παράδειγμα, αυτό μπορεί να έχει ανεπιθύμητη τροποποίηση δεδομένων εάν οι δύο μέθοδοι εκτελούνται από διαφορετικά θέματα:

val list = mutableListOf (1, 2)
suspend fun ενημέρωσηList1 () {
  λίστα [0] = λίστα [0] + 1
}}
suspend fun ενημέρωσηList2 () {
  list.clear ()
}}

Μπορείτε να αποφύγετε αυτό το είδος προβλήματος:
- έχοντας τα coroutines σας επιστρέφει ένα αμετάβλητο αντικείμενο αντί να αγγίξει και να αλλάξει ένα
- να εκτελέσετε όλες αυτές τις coroutines σε ένα ενιαίο threaded πλαίσιο που δημιουργήθηκε μέσω: newSingleThreadContext ("contextname")

5. Κάντε Proguard Happy

Αυτά θα πρέπει να προστεθούν κανόνες για τις εκδόσεις της εφαρμογής σας:

-διατηρεί τάξη kotlinx.coroutines.internal.MainDispatcherFactory {}
-διατηρεί τάξη kotlinx.coroutines.CoroutineExceptionHandler {}
-μεταξύ κλάδωνμεταξύ κλάσεων kotlinx. ** {volatile ; }}

6. Interop με Java

Εάν εργάζεστε σε μια εφαρμογή παλαιού τύπου, θα έχετε χωρίς αμφιβολία ένα σημαντικό κομμάτι κώδικα Java. Μπορείτε να καλέσετε coroutines από την Java επιστρέφοντας ένα CompletableFuture (βεβαιωθείτε ότι έχετε συμπεριλάβει το artifact του kotlinx-coroutines-jdk8):

doSomethingAsync (): CompletableFuture <Λίστα > =
   GlobalScope.future {doSomething ()}

7. Επανόρθωση δεν χρειάζεστε μεContext

Αν χρησιμοποιείτε τον προσαρμογέα Retrofit coroutines, λαμβάνετε ένα Deferred που χρησιμοποιεί την κλήση async του okhttp κάτω από την κουκούλα. Έτσι, δεν χρειάζεται να προσθέσετε μεContext (Dispatchers.IO) όπως θα έπρεπε να κάνετε με το RxJava για να βεβαιωθείτε ότι ο κώδικας τρέχει σε ένα νήμα IO? εάν δεν χρησιμοποιείτε τον προσαρμογέα Retrofit coroutines και καλείτε απευθείας μια κλήση Retrofit, χρειάζεστε τοContext.

Το Android Arch Components Room DB επίσης λειτουργεί αυτόματα σε περιβάλλον εκτός UI, επομένως δεν χρειάζεστε τοContext.

Βιβλιογραφικές αναφορές:

  • διαφήμιση
  • https://speakerdeck.com/elizarov/fresh-async-with-kotlin
  • https://medium.com/@michaelbukachi/coroutines-and-idling-resources-c1866bfa5b5d
  • https://blog.kotlin-academy.com/kotlin-coroutines-cheat-sheet-8cf1e284dc35
  • https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5?linkId=63267803
  • https://proandroiddev.com/managing-exceptions-in-nested-coroutine-scopes-9f23fd85e61