Programação Assíncrona com Coroutines & Channels

 

Natan Streppel

  • Software Developer com 5 anos de experiência atualmente atuando com Go & Kotlin

 

Programação Assíncrona

 

 Coroutines são uma ferramenta para programação assíncrona

 

Assincronia

=

eficiência 

not exactly...

o mundo real da TI atualmente é mais concorrente que paralelo

Assincronismo

!=

Paralelismo

Apesar de conseguirmos atingir paralelismo com coroutines...

... esta não é sua principal finalidade

Concorrência: composição de processos (sent. figurado) independentes

Paralelismo: execução de processos

ao mesmo tempo

"[...] concurrency is the occurrence of events independent of the main program flow."

Coroutines

"lightweight threads"

*(kind of)

 

 

Threads podem custar 1MiB ~ 2MiB* 

Coroutines usam algumas centenas de bytes**

Além disso, o modo de processamento é diferente

 

Coroutines: state machines

 

  • Dados do estado atual 

  • Habilidade de "dormir" 

  • Index do estado atual

Coroutines são, conceitualmente, pedaços de código que podem se suspender ("dormir") voluntariamente 

e, na prática, são uma library

 

https://github.com/Kotlin/kotlinx.coroutines

standard library

kotlinx-coroutines

launch, async, runBlocking, future, delay, Job, Deferred...

Não estamos nos livrando de Threads...

... estamos distribuindo pequenos "objetos" para melhor aproveitar elas

 

Podemos resolver assincronia usando callbacks

fun postItem(item: Item) {
    preparePostAsync { token -> 
        submitPostAsync(token, item) { post -> 
            processPost(post)
        }
    }
}

mas se precisarmos alterar o fluxo...

fun postItem(item: Item) {
    preparePostAsync { token -> 
        submitPostAsync(token, item) { post -> 
            processPost(post)
        }
    }
}
fun postItem(item: Item) {
   validateWithExternalService { item ->
      preparePostAsync { token -> 
          submitPostAsync(token, item) { post -> 
              processPost(post)
          }
      }    
   }
}
fun postItem(item: Item) {
   validateWithExternalService { item ->
      preparePostAsync { token -> 
          submitPostAsync(token, item) { post -> 
              processPost(post)
          }
      }    
   }
}
  • Callback hell; crescimento horizontal
  • Bug-friendly
  • Tende somente à crescer
  • Não lê "naturalmente"

Ou, também, com Futures/Promises/Etc 

fun postItem(item: Item) {
    preparePostAsync()
        .thenCompose { token -> 
            submitPostAsync(token, item)
        }
        .thenAccept { post -> 
            processPost(post)
        }

}
fun postItem(item: Item) {
   validateWithExternalService(item)
        .thenCompose { item ->
            preparePostAsync(item) 
        }
        .thenCompose { token -> 
            submitPostAsync(token, item)
        }
        .thenAccept { post -> 
            processPost(post)
        }
}
fun postItem(item: Item) {
   validateWithExternalService(item)
        .thenCompose { item ->
            preparePostAsync(item) 
        }
        .thenCompose { token -> 
            submitPostAsync(token, item)
        }
        .thenAccept { post -> 
            processPost(post)
        }
}
  • Ainda é um modo diferente de pensar
  • .thenCompose() et al. podem mudar de library para library
  • Não trabalhamos mais com o tipo desejado, mas sim "futures"

Ou, também, com coroutines

fun postItem(item: Item) {
   launch {
      val token = preparePost()
      val post = submitPost(token, item)
      processPost(post)
   }
}

fun postItem(item: Item) {
   launch {
      val item = validateWithExternalService(item)
      val token = preparePost()
      val post = submitPost(token, item)
      processPost(post)
   }
}

fun postItem(item: Item) {
   launch {
      val item = validateWithExternalService(item)
      val token = preparePost()
      val post = submitPost(token, item)
      processPost(post)
   }
}

  • Lê de modo sequencial e natural
  • error handling limpo e models se mantém intactos
  • Os tipos se mantém; preparePost() retorna um token e não uma promise

Coroutines

Problema: fetches no Github

Temos um endpoint que retorna os repositórios de uma organization

https://api.github.com/orgs/{org}/repos

o retorno é um json paginado com infos dos repositórios da org

[
  {
    "id": 8856204,
    "node_id": "MDEwOlJlcG9zaXRvcnk4ODU2MjA0",
    "name": "kotlin-examples",
    "full_name": "Kotlin/kotlin-examples",
    "private": false,
    "owner": {
      "login": "Kotlin",
      "id": 1446536,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0NDY1MzY=",
      "avatar_url": "https://avatars3.githubusercontent.com/u/1446536?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Kotlin",
      "html_url": "https://github.com/Kotlin",
      "followers_url": "https://api.github.com/users/Kotlin/followers",
      "following_url": "https://api.github.com/users/Kotlin/following{/other_user}",
      "gists_url": "https://api.github.com/users/Kotlin/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/Kotlin/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/Kotlin/subscriptions",
      "organizations_url": "https://api.github.com/users/Kotlin/orgs",
      "repos_url": "https://api.github.com/users/Kotlin/repos",
      "events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
      "received_events_url": "https://api.github.com/users/Kotlin/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "html_url": "https://github.com/Kotlin/kotlin-examples",
    "description": "Various examples for Kotlin",
    "fork": false,
    "url": "https://api.github.com/repos/Kotlin/kotlin-examples",
    "forks_url": "https://api.github.com/repos/Kotlin/kotlin-examples/forks",
    "keys_url": "https://api.github.com/repos/Kotlin/kotlin-examples/keys{/key_id}",
    "repos_url": "https://api.github.com/users/Kotlin/repos",
    "events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Kotlin/received_events",
[
  {
    "id": 8856204,
    "node_id": "MDEwOlJlcG9zaXRvcnk4ODU2MjA0",
    "name": "kotlin-examples",
    "full_name": "Kotlin/kotlin-examples",
    "private": false,
    "owner": {
      "login": "Kotlin",
      "id": 1446536,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0NDY1MzY=",
      "avatar_url": "https://avatars3.githubusercontent.com/u/1446536?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Kotlin",
      "html_url": "https://github.com/Kotlin",
      "followers_url": "https://api.github.com/users/Kotlin/followers",
      "following_url": "https://api.github.com/users/Kotlin/following{/other_user}",
      "gists_url": "https://api.github.com/users/Kotlin/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/Kotlin/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/Kotlin/subscriptions",
      "organizations_url": "https://api.github.com/users/Kotlin/orgs",
      "repos_url": "https://api.github.com/users/Kotlin/repos",
      "events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
      "received_events_url": "https://api.github.com/users/Kotlin/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "html_url": "https://github.com/Kotlin/kotlin-examples",
    "description": "Various examples for Kotlin",
    "fork": false,
    "url": "https://api.github.com/repos/Kotlin/kotlin-examples",
    "forks_url": "https://api.github.com/repos/Kotlin/kotlin-examples/forks",
    "keys_url": "https://api.github.com/repos/Kotlin/kotlin-examples/keys{/key_id}",
    "repos_url": "https://api.github.com/users/Kotlin/repos",
    "events_url": "https://api.github.com/users/Kotlin/events{/privacy}",
    "received_events_url": "https://api.github.com/users/Kotlin/received_events",

Para cada repo, vamos pegar os contribuintes...

... e somar suas contribuições para gerar um relatório no final

PRA DEMITIR QUEM COMMITOU MENOS DE 50 VEZES SEMANA PASSADA

[
  {
    "login": "jonnyzzz",
    "id": 256431,
    "node_id": "MDQ6VXNlcjI1NjQzMQ==",
    "avatar_url": "https://avatars3.githubusercontent.com/u/256431?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/jonnyzzz",
    "html_url": "https://github.com/jonnyzzz",
    "followers_url": "https://api.github.com/users/jonnyzzz/followers",
    "following_url": "https://api.github.com/users/jonnyzzz/following{/other_user}",
    "gists_url": "https://api.github.com/users/jonnyzzz/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/jonnyzzz/starred{/owner}{/repo}",
    "repos_url": "https://api.github.com/users/jonnyzzz/repos",
    "events_url": "https://api.github.com/users/jonnyzzz/events{/privacy}",
    "received_events_url": "https://api.github.com/users/jonnyzzz/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 54
  },
  {
    "login": "yanex",
    "id": 95996,
    "node_id": "MDQ6VXNlcjk1OTk2",
    "avatar_url": "https://avatars2.githubusercontent.com/u/95996?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/yanex",
    "html_url": "https://github.com/yanex",
    "followers_url": "https://api.github.com/users/yanex/followers",
    "following_url": "https://api.github.com/users/yanex/following{/other_user}",
    "gists_url": "https://api.github.com/users/yanex/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/yanex/starred{/owner}{/repo}",
    "repos_url": "https://api.github.com/users/yanex/repos",
    "events_url": "https://api.github.com/users/yanex/events{/privacy}",
    "received_events_url": "https://api.github.com/users/yanex/received_events",
    "type": "User",
    "site_admin": false,
    "contributions": 47
  },
  {
    "login": "yole",
    "id": 46553,

Solução inicial: síncrona

(utilizaremos o HTTP client retrofit)

Interface HTTP

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    fun getOrgReposCall(
        @Path("org") org: String
    ): Call<List<Repo>>

    @GET("repos/{org}/{repo}/contributors?per_page=100")
    fun getRepoContributorsCall(
        @Path("org") org: String,
        @Path("repo") repo: String
    ): Call<List<User>>
}

E resolvemos as chamadas assim

val users = loadContributorsBlocking(service, req)
updateResults(users)

Onde loadContributorsBlocking() é....

fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
    val repos: list<Repo> = service
        .getOrgReposCall(req.org)
        .execute() // blocks current thread
        .also { logRepos(req, it) }
        .body() ?: listOf()

    return repos.flatMap { repo ->
        service
            .getRepoContributorsCall(req.org, repo.name)
            .execute() // blocks current thread
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}

Tempo com 46 requests:

 ~14 sec

Podemos melhorar?

Podemos tentar fazer as outras 46 requisições concorrentemente

Então vamos suspender essas chamadas enquanto não retornam!

Mas como um código pode ser suspenso?

keyword suspend

!!! 

keyword suspend aplicada antes de uma função

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    fun getOrgReposCall(
        @Path("org") org: String
    ): Call<List<Repo>>

    @GET("repos/{org}/{repo}/contributors?per_page=100")
    fun getRepoContributorsCall(
        @Path("org") org: String,
        @Path("repo") repo: String
    ): Call<List<User>>
}
interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgReposCall(
        @Path("org") org: String
    ): Call<List<Repo>>

    @GET("repos/{org}/{repo}/contributors?per_page=100")
    suspend fun getRepoContributorsCall(
        @Path("org") org: String,
        @Path("repo") repo: String
    ): Call<List<User>>
}
fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    return repos.flatMap { repo ->
        service
            .getRepoContributors(req.org, repo.name)
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    return repos.flatMap { repo ->
        service
            .getRepoContributors(req.org, repo.name)
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}

e vamos chamar loadContributorsSuspend de uma coroutine agora

Mas como de fato construímos uma coroutine?

Coroutine Builders

Por default, temos 3 builders disponíveis

  • Launch          - fire and forget
  • Async             - fire mas quero saber o retorno (utilizando deferreds ("promises"))
  • runBlocking  - fire e tranca a thread em que está rodando até todas as coroutines "filhas" terminarem

Sempre que tivermos um código que pode ser suspenso, podemos usar um desses builders pra lançar!

Subtitle

val users = loadContributorsSuspend(service, req)
updateResults(users)
launch {
    val users = loadContributorsSuspend(service, req)
    updateResults(users, startTime)
}

agora vai que vai  segura peão

Tempo com 46 requests e suspend functions:

 ~13 sec

 

why 

Nós criamos suspending functions

E chamamos elas de uma coroutine diferente

Mas as chamadas HTTP não foram feitas de modo concorrente

suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    return repos.flatMap { repo ->
        service
            .getRepoContributors(req.org, repo.name)
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    return repos.flatMap { repo ->
        service
            .getRepoContributors(req.org, repo.name)
            .also { logUsers(repo, it) }
            .bodyList()
    }.aggregate()
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()

    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
}

o objeto deferred tem o método await()

suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
}

uma list de deferreds tem o método awaitAll()

suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
    deferreds.awaitAll()
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
    deferreds.awaitAll().flatten().aggregate()
}
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    // deferreds é uma List<Deferred<List<User>>>
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
    return deferreds.awaitAll().flatten().aggregate()
}

E agora?

Tempo com 46 requests e suspend functions assíncronas:

 ~5 sec

Uma melhora de ~65% no tempo de execução!

Estamos lidando com conexões HTTP pela internet (com a wifi da minha casa)

Em uma rede cloud gigabit ethernet a diferença pode (provavelmente vai) ser ainda maior

Show!

Mas podemos melhorar um pouco mais ainda...

a tela ainda fica vazia enquanto atualiza

poderíamos atualizar a GUI com os retornos on the fly, né?

Temos coroutines executando ao mesmo tempo da GUI

E a GUI pode rodar (e provavelmente roda) em outra thread

Como podemos comunicar o resultado das coroutines para a GUI?

Comunicação por channels

Channels são canais de comunicação entre coroutines

Podem receber e enviar entre diferentes produces e consumers

Como podemos implementar channels aqui?

suspend fun loadContributorsChannels(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
    return deferreds.awaitAll().flatten().aggregate()
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData): List<User> {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    /*val deferreds = repos.map { repo ->
          async {
             service
                .getRepoContributors(req.org, repo.name)
                .also { logUsers(repo, it) }
                .bodyList()
          }
    }
    return deferreds.awaitAll().flatten().aggregate()*/
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) {
        launch {
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users)
        }
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive() 
        allUsers = (allUsers + users).aggregate()

    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive() // pra cada mensagem recebida 
        allUsers = (allUsers + users).aggregate() // somamos os resultados
       // mas como atualizamos a GUI??
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive() // pra cada mensagem recebida 
        allUsers = (allUsers + users).aggregate() // somamos os resultados
       // vamos chamar a função de atualização aqui, recebendo ela como parametro nessa função aqui!
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive() 
        allUsers = (allUsers + users).aggregate()
        //TODO: update GUI
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData, updateResults: suspend (List<User>, completed: Boolean)) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive() 
        allUsers = (allUsers + users).aggregate()
        //TODO: update GUI
    }
}
suspend fun loadContributorsChannels(service: GitHubService, req: RequestData, updateResults: suspend (List<User>, completed: Boolean)) {
    val repos = service
        .getOrgRepos(req.org)
        .also { logRepos(req, it) }
        .body() ?: listOf()
    val channel = Channel<List<User>>() 
    for (repo in repos) { // pra cada repo
        launch { // lançamos uma coroutine nova
           val users = service
                        .getRepoContributors(req.org, repo.name)
                        .bodyList()
            channel.send(users) // e enviamos o result aqui
        }
    }
    var allUsers = emptyList<User>()
    repeat(repos.size) {
        val users = channel.receive()
        allUsers = (allUsers + users).aggregate()
        updateResults(allUsers, it == repos.lastIndex)
    }
}
launch {
    val users = loadContributorsChannels(service, req)
    updateResults(users, startTime)
}
launch {
    val users = loadContributorsChannels(service, req) { users, completed ->
        updateResults(users, completed)   
    }
}
launch {
    loadContributorsChannels(service, req) { users, completed ->
        updateResults(users, completed)   
    }
}

Essa talk foi inspirada na página Introduction to Coroutines & Channels

Coroutines & Channels (K/E)

By Natan Streppel

Coroutines & Channels (K/E)

presentation for the Kotlin Everywhere event based in Curitiba (2019)

  • 296