Kotlin Multiplatform. Nous travaillons avec le multithreading dans la pratique. Partie 2

Bonne journée à tous! Je suis avec vous, Anna Zharkova, une des principales développeurs mobiles chez Usetech.

Dans l'article précédent, j'ai parlé de l'un des moyens d'implémenter le multithreading dans une application Kotlin Multiplatform. Aujourd'hui, nous allons considérer une situation alternative lorsque nous implémentons une application avec le code commun le plus partagé, transférant tout le travail avec des threads dans une logique commune.





Dans l'exemple précédent, nous avons été aidés par la bibliothèque Ktor, qui a repris tout le travail principal de fournir une asynchronie dans le client réseau. Cela nous a évité d'avoir à utiliser DispatchQueue sur iOS dans ce cas particulier, mais dans d'autres, nous devions utiliser un travail de file d'attente d'exécution pour appeler la logique métier et traiter la réponse. Du côté Android, nous avons utilisé MainScope pour appeler une fonction suspendue.



Donc, si nous voulons implémenter un travail uniforme avec le multithreading dans un projet commun, nous devons configurer correctement la portée et le contexte de la coroutine dans laquelle il sera exécuté.

Commençons simplement. Créons notre médiateur architectural, qui appellera des méthodes de service sur sa portée, obtenues à partir du contexte coroutine:

class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope {
    private var onViewDetachJob = Job()
    override val coroutineContext: CoroutineContext = context + onViewDetachJob

    fun viewDetached() {
        onViewDetachJob.cancel()
    }
}

//   
abstract class BasePresenter(private val coroutineContext: CoroutineContext) {
    protected var view: T? = null
    protected lateinit var scope: PresenterCoroutineScope

    fun attachView(view: T) {
        scope = PresenterCoroutineScope(coroutineContext)
        this.view = view
        onViewAttached(view)
    }
}

      
      





Nous appelons le service dans la méthode médiateur et le transmettons à notre interface utilisateur:

class MoviesPresenter:BasePresenter(defaultDispatcher){
    var view: IMoviesListView? = null

    fun loadData() {
        //  
        scope.launch {
            service.getMoviesList{
                val result = it
                if (result.errorResponse == null) {
                    data = arrayListOf()
                    data.addAll(result.content?.articles ?: arrayListOf())
                    withContext(uiDispatcher){
                    view?.setupItems(data)
                   }
                }
            }
        }

//IMoviesListView - /,    UIViewController  Activity. 
interface IMoviesListView  {
  fun setupItems(items: List<MovieItem>)
}
class MoviesVC: UIViewController, IMoviesListView {
private lazy var presenter: IMoviesPresenter? = {
       let presenter = MoviesPresenter()
        presenter.attachView(view: self)
        return presenter
    }()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        presenter?.attachView(view: self)
        self.loadMovies()
    }

    func loadMovies() {
        self.presenter?.loadMovies()
    }

   func setupItems(items: List<MovieItem>){}
//....

class MainActivity : AppCompatActivity(), IMoviesListView {
    val presenter: IMoviesPresenter = MoviesPresenter()

    override fun onResume() {
        super.onResume()
        presenter.attachView(this)
        presenter.loadMovies()
    }

   fun  setupItems(items: List<MovieItem>){}
//...

      
      







Pour créer correctement une étendue à partir d'un contexte de coroutine, nous devons configurer un répartiteur de coroutine.

Cette logique dépend de la plate-forme, nous utilisons donc la personnalisation avec expect / actual.

expect val defaultDispatcher: CoroutineContext

expect val uiDispatcher: CoroutineContext

      
      





uiDispatcher sera responsable du travail sur le thread de l'interface utilisateur. defaultDispatcher sera utilisé pour travailler en dehors du thread d'interface utilisateur.

Le moyen le plus simple de le créer est dans androidMain, car la JVM Kotlin a des implémentations prêtes à l'emploi pour les répartiteurs coroutine. Pour accéder aux flux correspondants, nous utilisons CoroutineDispatchers Main (flux UI) et Default (standard pour Coroutine):

actual val uiDispatcher: CoroutineContext
    get() = Dispatchers.Main

actual val defaultDispatcher: CoroutineContext
    get() = Dispatchers.Default
      
      







Le MainDispatcher est sélectionné pour la plate-forme sous le capot du CoroutineDispatcher à l'aide de l'usine de répartition MainDispatcherLoader:



internal object MainDispatcherLoader {

    private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = if (FAST_SERVICE_LOADER_ENABLED) {
                FastServiceLoader.loadMainDispatcherFactory()
            } else {
                // We are explicitly using the
                // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
                // form of the ServiceLoader call to enable R8 optimization when compiled on Android.
                ServiceLoader.load(
                        MainDispatcherFactory::class.java,
                        MainDispatcherFactory::class.java.classLoader
                ).iterator().asSequence().toList()
            }
            @Suppress("ConstantConditionIf")
            factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: createMissingDispatcher()
        } catch (e: Throwable) {
            // Service loader can throw an exception as well
            createMissingDispatcher(e)
        }
    }
}

      
      







C'est la même chose avec Default:

internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO: CoroutineDispatcher = LimitingDispatcher(
        this,
        systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
        "Dispatchers.IO",
        TASK_PROBABLY_BLOCKING
    )

    override fun close() {
        throw UnsupportedOperationException("$DEFAULT_DISPATCHER_NAME cannot be closed")
    }

    override fun toString(): String = DEFAULT_DISPATCHER_NAME

    @InternalCoroutinesApi
    @Suppress("UNUSED")
    public fun toDebugString(): String = super.toString()
}

      
      







Cependant, toutes les plates-formes n'ont pas d'implémentations de répartiteur coroutine. Par exemple, pour iOS, qui fonctionne avec Kotlin / Native, pas Kotlin / JVM.

Si nous essayons d'utiliser le code, comme dans Android, nous obtiendrons une erreur: voyons ce que nous





faisons.



Le numéro 470 de GitHub Kotlin Coroutines contient des informations selon lesquelles les répartiteurs spéciaux n'ont pas encore été implémentés pour iOS: le





numéro 462 , dont 470 dépend, le même toujours dans l'état Ouvert: la





solution recommandée est de créer vos propres répartiteurs pour iOS:

actual val defaultDispatcher: CoroutineContext
get() = IODispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher

private object MainDispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run()
            }catch (err: Throwable) {
                throw err
            }
        }
    }
}

private object IODispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(),
0.toULong())) {
            try {
                block.run()
            }catch (err: Throwable) {
                throw err
            }
        }
    }

      
      







Nous obtiendrons la même erreur au démarrage.

Premièrement, nous ne pouvons pas utiliser dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong (), 0.toULong ())), car il n'est lié à aucun thread dans Kotlin / Native:



Deuxièmement, Kotlin / Native contrairement à Kotlin / JVM ne peut pas tâtonner les coroutines entre les threads. Et aussi tous les objets mutables.

Par conséquent, nous utilisons MainDispatcher dans les deux cas:

actual val ioDispatcher: CoroutineContext
get() = MainDispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher


@ThreadLocal
private object MainDispatcher: CoroutineDispatcher(){
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run().freeze()
            }catch (err: Throwable) {
                throw err
            }
        }
    }

      
      







Pour que nous puissions transférer des blocs de code et des objets mutables entre les threads, nous devons les figer avant de les transférer à l' aide de la commande freeze ():



Cependant, si nous essayons de figer un objet déjà gelé, par exemple, des singletons, qui sont considérés comme gelés par défaut, nous obtenons FreezingException.

Pour éviter que cela ne se produise, nous marquons les singletons avec l'annotation @ThreadLocal et les variables globales @SharedImmutable:

/**
 * Marks a top level property with a backing field or an object as thread local.
 * The object remains mutable and it is possible to change its state,
 * but every thread will have a distinct copy of this object,
 * so changes in one thread are not reflected in another.
 *
 * The annotation has effect only in Kotlin/Native platform.
 *
 * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
 */
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public actual annotation class ThreadLocal

/**
 * Marks a top level property with a backing field as immutable.
 * It is possible to share the value of such property between multiple threads, but it becomes deeply frozen,
 * so no changes can be made to its state or the state of objects it refers to.
 *
 * The annotation has effect only in Kotlin/Native platform.
 *
 * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES.
 */
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
public actual annotation class SharedImmutable

      
      







Utiliser MainDispatcher dans les deux cas est très bien lorsque vous travaillez avec Ktor. Si nous voulons que nos demandes lourdes passent en arrière-plan, nous pouvons les envoyer au GlobalScope avec le répartiteur principal Dispatchers.Main / MainDispatcher comme contexte:

iOS

actual fun ktorScope(block: suspend () -> Unit) {
    GlobalScope.launch(MainDispatcher) { block() }
}

      
      







Android:

actual fun ktorScope(block: suspend () -> Unit) {
           GlobalScope.launch(Dispatchers.Main) { block() }
       }

      
      





L'appel et le changement de contexte seront alors à notre service:

suspend fun loadMovies(callback:(MoviesList?)->Unit) {
       ktorScope {
            val url =
                "http://api.themoviedb.org/3/discover/movie?api_key=KEY&page=1&sort_by=popularity.desc"
            val result = networkService.loadData<MoviesList>(url)
            delay(1000)
           withContext(uiDispatcher) {
               callback(result)
           }
        }
    }

      
      





Et même si vous appelez non seulement la fonctionnalité Ktor là-bas, tout fonctionnera.



Vous pouvez également implémenter sur iOS un appel de blocage avec un transfert vers l'arrière-plan DispatchQueue comme ceci:

//  ,  ,   
actual fun callFreeze(callback: (Response)->Unit) {
    val block = {
      //     ,    
        callback(Response("from ios").freeze())
    }
    block.freeze()
    dispatch_async {
        queue = dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND.toLong, 
            0.toULong())
        block = block     
    }
}

      
      







Bien sûr, vous devrez également ajouter un callFreeze (...) amusant du côté Android, mais simplement en passant votre réponse au rappel.



En conséquence, après avoir effectué toutes les modifications, nous obtenons une application qui fonctionne de la même manière sur les deux plates-formes:





Exemple de sources github.com/anioutkazharkova/movies_kmp

Il existe un exemple similaire, mais pas sous Kotlin 1.4

github.com/anioutkazharkova/kmp_news_sample



tproger.ru/articles/creating-an -app-for-kotlin-multiplatform

github.com/JetBrains/kotlin-native

github.com/JetBrains/kotlin-native/blob/master/IMMUTABILITY.md

github.com/Kotlin/kotlinx.coroutines/issues/462

helw.net / 2020/04/16 / multithreading-dans-les-applications-multiplateformes-kotlin






All Articles