Para usufruir dos múltiplos núcleos existentes nos processadores dos smartphones atuais, podemos realizar chamadas assíncronas de modo a paralelizar o fluxo de execução da aplicação. Normalmente isso é feito por meio de threads e callbacks que acabam por adicionar uma complexidade ao código que pode comprometer sua leitura e manutenção. Nessa apresentação, veremos como utilizar a API de Coroutines do Kotlin em conjunto com diversas bibliotecas do Jetpack do Android de modo a implementar programação assíncrona forma simples e eficiente.
7. Coroutines
• Essencialmente, coroutines são light-weight threads.
• Fácil de usar (sem mais "callbacks hell” e/ou centenas de
operadores).
• Úteis para operações de I/O ou qualquer outra tarefa
computacional mais onerosa.
• Permite a substituição de callbacks por operações
assíncronas.
9. suspend
• Suspending functions são o centro de tudo em Coroutines.
Elas são basicamente funções que podem ser pausadas e
retomadas após algum tempo.
• Podem executar longas tarefas e aguardar o resultado sem
bloquear a thread atual.
• A sintaxe é idêntica a uma função “normal”, exceto pela
adição da palavra reservada suspend.
• Por si só, uma suspending function não é assíncrona.
• Só pode ser chamada a partir de outra suspending function.
10. import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.Assert.assertEquals
class CalculatorUnitTest {
@Test
fun sum_isCorrect() = runBlocking {
val calc = Calculator()
assertEquals(4, calc.sum(2, 2))
}
}
import kotlinx.coroutines.delay
class Calculator {
suspend fun sum(a: Int, b: Int): Int {
delay(5_000)
return a + b
}
}
11. Coroutines no Android
• Para entendermos melhor as Coroutines no Android
precisamos falar de 4 conceitos:
• Job
• Context
• Scope
• Dispatcher
12. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
13. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
Job
• Um Job representa uma tarefa ou conjunto de
tarefas em background.
• A função launch retorna um Job.
• Mantém a referência do código que está em
execução.
• Pode possuir “filhos”.
• Pode ser cancelado usando a função cancel.
14. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
Context
• É um conjunto de atributos que configuram
uma coroutine.
• Representada pela interface
CoroutineContext.
• Pode definir a política de threading,
tratamento de exceções, etc.
15. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
Scope
• Um escopo serve como uma espécie de ciclo de
vida para um conjunto de coroutines.
• Coroutines sempre rodam em um escopo.
• Permite um maior controle das tarefas em
execução
16. GlobalScope
• O GlobalScope, como o nome sugere, é utilizado em tarefas cujo o escopo é
global da aplicação.
• O GlobalScope é considerado um anti-pattern no Android e deve ser evitado.
private suspend fun loadName(): String {
delay(5000)
return "Glauber"
}
private fun firstCoroutine() {
GlobalScope.launch {
val name = loadName()
println("$name!")
}
println("Hello, ")
}
17. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
Dispatcher
• Um dispatcher define o pool de threads
que a coroutine executará.
• Default - É otimizado para processos
que usam a CPU mais intensamente.
• IO - recomendado para tarefas de rede
ou arquivos. O pool de threads é
compartilhado com o dispatcher
DEFAULT.
• Main - main thread do Android.
19. fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = BookHttp.loadBooks()
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1513)
at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:117)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:105)
at java.net.InetAddress.getAllByName(InetAddress.java:1154)
at com.android.okhttp.Dns$1.lookup(Dns.java:39)
20. FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: br.com.nglauber.coroutinesdemo, PID: 26507
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that
created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
fun callWebService() {
coroutineScope.launch(Dispatchers.IO){
txtOutput.text = ""
val books = BookHttp.loadBooks()
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
21. class MainActivity : AppCompatActivity() {
private val job = Job()
private val coroutineScope = CoroutineScope(job + Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
22. class MainActivity : AppCompatActivity(), CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
fun callWebService() {
launch {
txtOutput.text = ""
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
books?.forEach {
txtOutput.append("${it.title}n")
}
}
}
...
24. Lifecycle Scope
• Escopo atrelado ao ciclo de vida da Activity, Fragment ou
View do Fragment
• Além da função launch, podemos usar o
launchWhenCreated, launchWhenStarted e
launchWhenResumed.
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01"
}
34. class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
val output = inputData.run {
val x = getInt("x", 0)
val y = getInt("y", 0)
val result = Calculator().sum(x, y)
workDataOf("result" to result)
}
Result.success(output)
} catch (error: Throwable) {
Result.failure()
}
}
private fun callMyWork() {
val request =
OneTimeWorkRequestBuilder<MyWork>()
.setInputData(workDataOf("x" to 84, "y" to 12))
.build()
WorkManager.getInstance(this).run {
enqueue(request)
getWorkInfoByIdLiveData(request.id)
.observe(this@MainActivity, Observer {
if (it.state == WorkInfo.State.SUCCEEDED) {
val result = it.outputData.getInt("result", 0)
addTextToTextView("Result-> $result")
}
})
}
36. Iniciando uma coroutine
• As duas formas de iniciar uma coroutine são:
• A função launch é uma “fire and forget” que significa
que não retornará o resultado para que a chamou (mas
retornará um Job).
• A função async retorna um objeto Deferred que permite
obter o seu resultado.
37. launch = Sequencial
private fun sequentialCalls() {
txtOutput.text = ""
launch {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
addTextToTextView("The answer is ${one + two}")
}
addTextToTextView("Completed in $time ms")
}
}
The answer is 42
Completed in 2030 ms
38. async = Paralelo
private fun parallelCalls() {
txtOutput.text = ""
launch {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
addTextToTextView("The answer is ${one.await() + two.await()} ")
}
addTextToTextView("Completed in $time ms")
}
}
The answer is 42
Completed in 1038 ms
39. Launch x Async
Em quase todos os casos, deve-se usar a função
launch para iniciar uma coroutine a partir de uma
função “normal”.
Uma função comum não pode chamar o await
(porque ela é uma suspending function), então
não faz muito sentido usar o async como ponto
de partida.
46. Cancelamento
• Para cancelar um job, basta chamar o método cancel.
• Uma vez cancelado o job não pode ser reusado.
• Para cancelar os jobs filhos, use cancelChildren.
• A propriedade isActive indica que o job está em
execução, isCancelled cancelado, e isCompleted
terminou sua execução.
• Se cancelado enquanto suspenso, levanta
CancellationException.
47. private fun cancelDemo() {
val startTime = System.currentTimeMillis()
longJob = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 20 && isActive) { // this == CoroutineScope, que possui isActive
if (System.currentTimeMillis() >= nextPrintTime) {
Log.d("NGVL","job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
}
49. withTimeout
• Executa uma coroutine levantando uma
TimeoutCancellationException caso sua duração
exceda o timeout especificado.
• Uma vez que o cancelamento é apenas uma exceção, é
possível trata-la facilmente.
• É possível usar a função withTimeoutOrNull que é similar
a withTimeout, mas retorna null ao invés de levantar a
exceção.
51. withTimeoutOrNull
private fun timeoutOrNullDemo() {
launch {
txtOutput.text = ""
val task = async(Dispatchers.Default) {
delay(5000)
"Done!"
}
// suspend until task is finished or return null in 2 sec
val result = withTimeoutOrNull(2000) { task.await() }
addTextToTextView("Result: $result") // ui thread
}
}
52. Converting Callbacks to
Coroutines
class LocationManager {
fun getCurrentLocation(callback: (LatLng?) -> Unit) {
// get the location...
callback(LatLng(-8.187,-36.156))
}
}
suspend fun getMyLocation(lm: LocationManager): LatLng {
return suspendCoroutine { continuation ->
lm.getCurrentLocation { latLng ->
if (latLng == null) {
continuation.resumeWithException(
Exception("Fail to get user location")
)
} else {
continuation.resume(latLng)
}
}
}
}
53. Converting Callbacks to
Coroutines
class LocationManager {
fun getCurrentLocation(callback: (LatLng?) -> Unit) {
// get the location...
callback(LatLng(-8.187,-36.156))
}
}
suspend fun getMyLocation(lm: LocationManager): LatLng {
return suspendCancellableCoroutine { continuation ->
lm.getCurrentLocation { latLng ->
if (latLng == null) {
continuation.resumeWithException(
Exception("Fail to get user location")
)
} else {
continuation.resume(latLng)
}
}
}
}
54. Nos bastidores, uma suspending function é
convertida pelo compilador para uma função (de
mesmo nome) que recebe um objeto do tipo
Continuation.
fun sum(a: Int, b: Int, Continuation<Int>)
Continuation é uma interface que contém duas funções que
são invocadas para continuar com a execução da coroutine
(normalmente retornando um valor) ou levantar uma exceção
caso algum erro ocorra.
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
56. Channel
• Serve basicamente para enviar dados de
uma coroutine para outra.
• O conceito de canal é similar a uma
blocking queue mas utiliza suspending
operations ao invés de blocking operations.
EXPERIMENTAL
57. 2019-06-11 21:08:41.988 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: 1
2019-06-11 21:08:41.990 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: 4
2019-06-11 21:08:42.005 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: 9
2019-06-11 21:08:42.005 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: 16
2019-06-11 21:08:42.051 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: 25
2019-06-11 21:08:42.051 5545-5545/br.com.nglauber.coroutinesdemo I/System.out: Done!
coroutineScope.launch {
val channel = Channel<Int>()
launch {
// this might be heavy CPU-consuming computation or
// async logic, we’ll just send five squares
for (x in 1..5) channel.send(x * x)
}
// here we print five received integers:
repeat(5) { println(channel.receive()) }
println("Done!")
}
60. Actors
• Basicamente, um ator é um elemento que
recebe mensagens por meio de um canal e
realiza algum trabalho baseado nelas.
• O retorno dessa função é um objeto do tipo
SendChannel que pode ser usado para
enviar mensagens para essa coroutine.
EXPERIMENTAL
61. class BooksWithActorViewModel : ViewModel() {
private var _screenState = MutableLiveData<State>()
val screenState: LiveData<State> = _screenState
private val actor: SendChannel<Action> = viewModelScope.actor {
for (action in this) when (action) {
is Action.LoadBooks -> doLoadBooks()
}
}
private suspend fun doLoadBooks() {
try {
_screenState.value = State.LoadingResults(true)
val books = withContext(Dispatchers.IO) {
BookHttp.loadBooks()
}
_screenState.value = State.BooksResult(books)
} catch (e: Exception) {
_screenState.value = State.ErrorResult(Exception(e))
} finally {
_screenState.value = State.LoadingResults(false)
}
}
fun loadBooksFromWeb() {
actor.offer(Action.LoadBooks)
}
override fun onCleared() {
super.onCleared()
actor.close()
}
}
62. class ActorDemoActivity : AppCompatActivity() {
private val viewModel: BooksWithActorViewModel by lazy { ... }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_actor_demo)
viewModel.screenState.observe(this, Observer { state ->
when (state) {
is State.LoadingResults -> showLoading(state.isLoading)
is State.BooksResult -> showResults(state.books)
is State.ErrorResult -> showError(state.t)
}
})
btnLoad.setOnClickListener {
viewModel.loadBooksFromWeb()
}
}
private fun showResults(books: List<Book>) {
txtResult.text = ""
books.forEach { txtResult.append("${it.title}n") }
}
private fun showLoading(show: Boolean) {
pbLoading.visibility = if (show) View.VISIBLE else View.GONE
}
private fun showError(t: Throwable) {
Toast.makeText(this, "${t.message}", Toast.LENGTH_SHORT).show()
}
}
63. Flow
• Flow é uma abstração de um cold stream.
• Nada é executado/emitido qualquer item até que algum
consumidor se registre no fluxo.
• Diversos operadores como no RxJava.
EXPERIMENTAL
64. @FlowPreview
public interface Flow<out T> {
public suspend fun collect(collector: FlowCollector<T>)
}
@FlowPreview
public interface FlowCollector<in T> {
public suspend fun emit(value: T)
}
65. private fun flowDemo() {
val intFlow = flow {
for (i in 0 until 10) {
emit(i) //calls emit directly from the body of a FlowCollector
}
}
launch {
txtOutput.text = ""
intFlow.collect { number ->
addTextToTextView("$numbern")
}
addTextToTextView("DONE!")
}
}
66. private fun flowDemo2() {
launch {
txtOutput.text = ""
(0..100).asFlow()
.map { it * it } // executed in IO
.filter { it % 4 == 0 } //executed in IO
.flowOn(Dispatchers.IO) //changes upstream context, asFlow, map and filter
.map { it * 2 } // not affected, continues in parent context
.flowOn(Dispatchers.Main)
.collect {number ->
addTextToTextView("$numbern")
}
}
}
67. import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
class NumberFlow {
private var currentValue = 0
private val numberChannel = BroadcastChannel<Int>(10)
fun getFlow(): Flow<Int> = numberChannel.asFlow()
suspend fun sendNext() {
numberChannel.send(currentValue++)
}
fun close() = numberChannel.close()
}
70. Room
dependencies {
def room_version = "2.1.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// 👇 Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
...
}
71. @Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid = :id")
suspend fun getUser(id: Long): User
@Insert
suspend fun insert(users: User): Long
@Delete
suspend fun delete(user: User)
}
72. @Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll(): ReceiveChannel<List<User>>
@Query("SELECT * FROM user WHERE uid = :id")
suspend fun getUser(id: Long): ReceiveChannel<User>
@Insert
suspend fun insert(users: User): Long
@Delete
suspend fun delete(user: User)
}
NOT
IMPLEMENTED.
YET?
73. @Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll(): Flow<List<User>>
@Query("SELECT * FROM user WHERE uid = :id")
suspend fun getUser(id: Long): Flow<User>
@Insert
suspend fun insert(users: User): Long
@Delete
suspend fun delete(user: User)
}
NOT
IMPLEMENTED.
YET?
76. LiveData
@Dao
interface UserDao {
...
@Query("SELECT * FROM user")
fun getLiveAll(): LiveData<List<User>>
}
val users: LiveData<List<User>> = liveData {
val data = dao.getLiveAll()
emitSource(data)
}
...
users.observe(this) { users ->
users.forEach { Log.d("NGVL", it.toString()) }
}
77. Conclusão
• Coroutines vêm se tornando a forma de padrão para
realizar código assíncrono no Android.
• Essa é uma recomendação do Google.
• Além do Jetpack, outras bibliotecas estão migrando pro
Jetpack (ex: Retrofit).
• Muita atenção para as APIs experimentais de hoje. Elas
podem ser o seu código de amanhã!