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.
2. • Carregar arquivo de layout
• Desenhar as views
• Tratar os eventos de UI
• …
A main thread do Android
3. • O processamento desses eventos deve ocorrer em:
• Menos de 16ms para devices com taxas de atualização
de 60Hz
• Menos de 12ms para dispositivos com taxas de 90Hz
• Menos de < 8ms para dispositivos com taxas de 120Hz
A main thread do Android
9. • Essencialmente, coroutines são light-weight threads.
• Fácil de usar (sem mais “callbacks hell” e/ou centenas de
operadores).
• Úteis para qualquer tarefa computacional mais onerosa
(como operações de I/O).
• Permite a substituição de callbacks por operações
assíncronas.
Coroutines
11. • Suspending functions são o centro de tudo em Coroutines.
• São 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.
suspend
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()
}
// update the UI using books
}
}
...
16. 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()
}
// update the UI using books
}
}
...
Job
• Um Job representa uma
tarefa ou conjunto de tarefas
em execução.
• Pode possuir “filhos”.
• A função launch retorna um
Job.
• Pode ser cancelado usando
a função cancel.
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()
}
// update the UI using books
}
}
...
Context
• A interface
CoroutineContext
Representa o conjunto de
atributos que configuram
uma coroutine.
• Pode definir a política de
threading, tratamento de
exceções, etc.
18. 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()
}
// update the UI using books
}
}
...
Scope
• Coroutines sempre rodam
em um escopo.
• Serve como uma espécie de
ciclo de vida para um
conjunto de coroutines.
• Permite um maior controle
das tarefas em execução.
19. 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()
}
// update the UI using books
}
}
...
Dispatcher
• Define o pool de threads
onde a coroutine executará.
• Default: para processos
que usam a CPU mais
intensamente.
• IO: para tarefas de rede
ou arquivos. O pool de
threads é compartilhado
com o dispatcher
DEFAULT.
• Main - main thread do
Android.
20. 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()
}
// update the UI using books
}
}
...
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()
}
// update the UI using books
}
}
...
23. fun callWebService() {
coroutineScope.launch {
txtOutput.text = ""
val books = BookHttp.loadBooks()
// update the UI using books
}
}
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)
24. fun callWebService() {
coroutineScope.launch(Dispatchers.IO){
txtOutput.text = ""
val books = BookHttp.loadBooks()
// update the UI using books
}
}
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)
28. • 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.
Lifecycle Scope
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-beta01"
}
36.
A classe ViewModel possui agora a propriedade viewModelScope.
ViewModel Scope
dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-beta01"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-beta01"
}
37. class MainViewModel : ViewModel() {
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun loadMessage() {
viewModelScope.launch {
val message = withContext(Dispatchers.IO) {
loadMessageFromNetwork()
}
_message.value = message
}
}
private suspend fun loadMessageFromNetwork() : String {
// load from web
}
}
38. class MainViewModel : ViewModel() {
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun loadMessage() {
viewModelScope.launch {
val message = withContext(Dispatchers.IO) {
loadMessageFromNetwork()
}
_message.value = message
}
}
private suspend fun loadMessageFromNetwork() : String {
// load from web
}
}
39. class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel.message.observe(this, Observer { s ->
txtOutput.text = s
})
viewModel.loadMessage()
}
...
40. class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel.message.observe(this, Observer { s ->
txtOutput.text = s
})
viewModel.loadMessage()
}
...
43. 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()
}
}
44. 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()
}
}
45. 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")
}
})
}
}
46. 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")
}
})
}
}
47. 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")
}
})
}
}
48. 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")
}
})
}
}
49. • Lifecycle provê um lifecycleScope para Activity e
Fragment (e a view do Fragment).
• ViewModel possui a propriedade viewModelScope.
• WorkManager disponibiliza a classe CoroutineWorker.
Jetpack + Coroutines
51. • 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.
Iniciando uma coroutine
52. launch
launch {
txtOutput.text = ""
val time = measureTimeMillis {
val one = withContext(Dispatchers.IO) { loadFirstNumber() }
val two = withContext(Dispatchers.IO) { loadSecondNumber() }
addTextToTextView("The answer is ${one + two}")
}
addTextToTextView("Completed in $time ms")
}
The answer is 42
Completed in 2030 ms
53. async
launch {
txtOutput.text = ""
val time = measureTimeMillis {
val one = async(Dispatchers.IO) { loadFirstNumber() }
val two = async(Dispatchers.IO) { loadSecondNumber() }
val s = one.await() + two.await()
addTextToTextView("The answer is $s")
}
addTextToTextView("Completed in $time ms")
}
54. async
launch {
txtOutput.text = ""
val time = measureTimeMillis {
val one = async(Dispatchers.IO) { loadFirstNumber() }
val two = async(Dispatchers.IO) { loadSecondNumber() }
val s = one.await() + two.await()
addTextToTextView("The answer is $s")
}
addTextToTextView("Completed in $time ms")
}
55. async
launch {
txtOutput.text = ""
val time = measureTimeMillis {
val one = async(Dispatchers.IO) { loadFirstNumber() }
val two = async(Dispatchers.IO) { loadSecondNumber() }
val s = one.await() + two.await()
addTextToTextView("The answer is $s")
}
addTextToTextView("Completed in $time ms")
}
56. async
The answer is 42
Completed in 1038 ms
launch {
txtOutput.text = ""
val time = measureTimeMillis {
val one = async(Dispatchers.IO) { loadFirstNumber() }
val two = async(Dispatchers.IO) { loadSecondNumber() }
val s = one.await() + two.await()
addTextToTextView("The answer is $s")
}
addTextToTextView("Completed in $time ms")
}
57. • O tratamento de exceções é simples, basta tratar no lugar
certo!
• As exceções não tratadas são propagadas para o Job do
escopo.
Exceptions
66. • 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 se a coroutine foi cancelada, e
isCompleted terminou sua execução.
Cancelamento
67. • Executa uma coroutine levantando uma
TimeoutCancellationException caso sua duração
exceda o tempo 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.
withTimeout
69. withTimeoutOrNull
launch {
txtOutput.text = ""
val task = async(Dispatchers.Default) {
aLongOperation()
}
val result = withTimeoutOrNull(1300L) { task.await() }
txtOutput.text = "Result: $result"
}
70. • 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)
}
Convertendo Callbacks
71. Convertendo Callbacks
object LocationManager {
fun getCurrentLocation(callback: (LatLng?) -> Unit) {
// get the location...
callback(LatLng(-8.187,-36.156))
}
}
LocationManager.getCurrentLocation { latLng ->
if (latLng != null) {
// Exibir localização
} else {
// Tratar o erro
}
}
72. Convertendo Callbacks
suspend fun getMyLocation(): LatLng {
return suspendCoroutine { continuation ->
LocationManager.getCurrentLocation { latLng ->
if (latLng != null) {
continuation.resume(latLng)
} else {
continuation.resumeWithException(
Exception("Fail to get user location")
)
}
}
}
}
73. Convertendo Callbacks
suspend fun getMyLocation(): LatLng {
return suspendCoroutine { continuation ->
LocationManager.getCurrentLocation { latLng ->
if (latLng != null) {
continuation.resume(latLng)
} else {
continuation.resumeWithException(
Exception("Fail to get user location")
)
}
}
}
}
74. Convertendo Callbacks
suspend fun getMyLocation(): LatLng {
return suspendCoroutine { continuation ->
LocationManager.getCurrentLocation { latLng ->
if (latLng != null) {
continuation.resume(latLng)
} else {
continuation.resumeWithException(
Exception("Fail to get user location")
)
}
}
}
}
76. • launch (fire-and-forget) e async (para obter um resultado).
• Trate as exceções no launch ou no async. Ou use
SupervisorJob, SupervisorScope ou coroutineScope.
• cancel ou cancelChildren para cancelar o Job ou os jobs
filhos.
• withTimeout ou withTimeoutOrNull.
• Toda suspend function é convertida em um callback usando a
interface Continuation.
Coroutines - Parte 2
79. 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!")
}
1
4
9
16
25
Done!
88. • Flow é uma abstração de um cold stream.
• Nada é executado/emitido até que algum consumidor se
registre no fluxo.
• Diversos operadores como no RxJava.
Flow
89. @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)
}
90. 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!")
}
91. launch {
txtOutput.text = ""
(0..100).asFlow()
.map { it * it }
.filter { it % 4 == 0 } // here and above is on IO thread pool
.flowOn(Dispatchers.IO) // 'change the upstream Dispatcher
.map { it * 2 }
.flowOn(Dispatchers.Main)
.collect {number ->
addTextToTextView("$numbern")
}
}
92. 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()
}
93. 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()
}
98. Room
dependencies {
def room_version = "2.2.0"
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"
...
}
99. @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)
}
100. @Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): Flow<List<User>>
@Query("SELECT * FROM user WHERE uid = :id")
fun getUser(id: Long): Flow<User?>
@Insert
suspend fun insert(users: User): Long
@Delete
suspend fun delete(user: User)
}
102. launch {
withContext(Dispatchers.IO) {
val id = dao.insert(
User(0,
edtFirstName.text.toString(),
edtLastName.text.toString())
)
}
edtFirstName.text.clear()
edtLastName.text.clear()
edtFirstName.requestFocus()
}
103. • 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
Coroutines (ex: Retrofit, Apollo, MockK, …).
Conclusão