No artigo anterior, falei sobre uma das maneiras de implementar multithreading em um aplicativo Kotlin Multiplatform. Hoje vamos considerar uma situação alternativa quando implementamos um aplicativo com o código comum mais compartilhado, transferindo todo o trabalho com threads para uma lógica comum.

No exemplo anterior, fomos ajudados pela biblioteca Ktor, que assumiu todo o trabalho principal de prover assincronia no cliente da rede. Isso nos livra de ter que usar DispatchQueue no iOS nesse caso específico, mas em outros teríamos que usar um trabalho de fila de execução para invocar a lógica de negócios e lidar com a resposta. No lado do Android, usamos MainScope para chamar uma função suspensa.
Assim, se quisermos implementar um trabalho uniforme com multithreading em um projeto comum, precisamos configurar corretamente o escopo e o contexto da co-rotina em que será executado.
Vamos começar de forma simples. Vamos criar nosso mediador de arquitetura, que chamará métodos de serviço em seu escopo, obtidos do contexto de co-rotina:
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)
}
}
Chamamos o serviço no método mediador e o passamos para nossa IU:
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>){}
//...
Para criar corretamente um escopo a partir de um contexto de co-rotina, precisamos configurar um despachante de co-rotina.
Essa lógica depende da plataforma, portanto, usamos a personalização com o esperado / real.
expect val defaultDispatcher: CoroutineContext
expect val uiDispatcher: CoroutineContext
uiDispatcher será responsável por trabalhar no thread de interface do usuário. defaultDispatcher será usado para trabalhar fora do thread da IU.
A maneira mais fácil de criá-lo é no androidMain, porque o Kotlin JVM tem implementações prontas para despachadores de corrotina. Para acessar os fluxos correspondentes, usamos CoroutineDispatchers Main (fluxo de UI) e Default (padrão para Coroutine):
actual val uiDispatcher: CoroutineContext
get() = Dispatchers.Main
actual val defaultDispatcher: CoroutineContext
get() = Dispatchers.Default
O MainDispatcher é selecionado para a plataforma sob o capô do CoroutineDispatcher usando a fábrica de despachante 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)
}
}
}
É o mesmo com o Padrão:
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()
}
No entanto, nem todas as plataformas têm implementações de dispatcher de corrotina. Por exemplo, para iOS, que funciona com Kotlin / Native, não Kotlin / JVM.
Se tentarmos usar o código, como no Android, obteremos um erro:

Vamos ver o que estamos fazendo.
Problema 470 do GitHub Kotlin Coroutines contém informações que os despachantes especiais ainda não foram implementados para iOS:

Problema 462 , do qual 470 depende, o mesmo ainda está no status Aberto: a

solução recomendada é criar seus próprios despachantes para 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
}
}
}
Obteremos o mesmo erro na inicialização.
Em primeiro lugar, não podemos usar dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong (), 0.toULong ())) porque não está vinculado a nenhum thread em Kotlin / Native:

Segundo, Kotlin / Native ao contrário do Kotlin / JVM não pode atrapalhar co-rotinas entre threads. E também quaisquer objetos mutáveis.
Portanto, usamos MainDispatcher em ambos os casos:
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
}
}
}
Para que possamos transferir blocos mutáveis de código e objetos entre threads, precisamos congelá- los antes da transferência usando o comando freeze ():

No entanto, se tentarmos congelar um objeto já congelado, por exemplo, singletons, que são considerados congelados por padrão, obteremos FreezingException.
Para evitar que isso aconteça, marcamos os singletons com a anotação @ThreadLocal e as variáveis globais @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
Usar MainDispatcher em ambos os casos é bom ao trabalhar com Ktor. Se quisermos que nossos pedidos pesados fiquem em segundo plano, podemos enviá-los ao GlobalScope com o despachante principal Dispatchers.Main / MainDispatcher como o contexto:
iOS
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(MainDispatcher) { block() }
}
Android:
actual fun ktorScope(block: suspend () -> Unit) {
GlobalScope.launch(Dispatchers.Main) { block() }
}
A chamada e a mudança de contexto estarão então ao nosso serviço:
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)
}
}
}
E mesmo se você chamar não apenas a funcionalidade Ktor lá, tudo funcionará.
Você também pode implementar no iOS uma chamada em bloco com uma transferência para o DispatchQueue em segundo plano como este:
// , ,
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
}
}
Claro, você terá que adicionar callFreeze (...) divertido real no lado do Android também, mas apenas passando sua resposta para o callback.
Como resultado, depois de fazer todas as edições, obtemos um aplicativo que funciona da mesma forma em ambas as plataformas:

Fontes de exemplo github.com/anioutkazharkova/movies_kmp
Há um exemplo semelhante, mas não em 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-in-kotlin-multiplataforma-apps