Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Training Program - Portugal, Aula 4

ATP Portugal
November 11, 2020

Android Training Program - Portugal, Aula 4

Aula #4: Fundações III 💪

Para a melhor experiência possível queremos que a nossa interface seja o mais rápida e fluida possível - para isso, operações pesadas devem ser delegadas para threads secundárias.

- Architecture Components
- Operações assíncronas
- Live data
- ViewModel
- Permissões

ATP Portugal

November 11, 2020
Tweet

More Decks by ATP Portugal

Other Decks in Education

Transcript

  1. Android
    Training
    Program
    PORTUGAL
    Aula #4
    Fundações III

    View Slide

  2. ● Sejam excelentes uns para os outros
    ● Fale mais alto se vir ou ouvir alguma coisa
    ● O assédio não é tolerado
    ● Pratique "Sim e" um ao outro
    Código de conduta
    Mais informações: http://bit.ly/2IhF0l3

    View Slide

  3. Andres-Leonardo
    Martinez-Ortiz
    Google
    Carlos Mota
    Formador
    Renato Almeida
    Formador
    @davilagrau @cafonsomota @tallnato
    Equipa
    Daniela Ferreira
    Gestora de
    comunidades

    View Slide

  4. ● 12 aulas
    ● 1h30 cada aula
    ● ~1 aula por semana
    ● 14 Outubro a 16 Dezembro
    ● YouTube live
    ● Suporte assíncrono contínuo via Discord/email
    ● Todo o código disponível no GitHub
    Photo by Arif Riyanto on Unspla
    O programa

    View Slide

  5. #0
    14 de Outubro
    Pronto para
    começar
    #1
    21 de Outubro
    Bem-vindos ao
    Android
    #2
    28 de Outubro
    Fundações I
    #3
    04 de Novembro
    Fundações II
    #4
    11 de Novembro
    Fundações III
    #5
    18 de Novembro
    Listas, listas e
    mais listas
    #6
    25 de Novembro
    Jetpack,
    Jetpack,
    Jetpack!
    #7 - #8
    02 - 03 de Dezembro
    Firebase
    #9 - #10
    09 - 10 de Dezembro
    MLKit &
    TensorFlow
    #11
    16 de Dezembro
    Resumo
    Semana Semana
    Calendário
    ✅ ✅ ✅
    Direto

    View Slide

  6. Photo by Mollie Sivaram on Unsplash

    View Slide

  7. Photo by Mollie Sivaram on Unsplash
    ‍ ~5000 participantes
    ~2600 subscrições no YouTube
    ~20000 visualizações
    Obrigado! ‍♀

    View Slide

  8. Sumário
    Photo by Mika Baumeister on Unsplash
    ● Resumo da aula anterior
    ● ViewModel
    ● LiveData
    ● Operações assíncronas
    ● Permissões
    ● Kotlin para principiantes
    ● Castanhas quentes e boas!

    View Slide

  9. http://events.withgoogle.com/atp2020
    [email protected]
    http://bit.ly/atp2020-youtube
    http://bit.ly/atp2020-discord
    Links

    View Slide

  10. http://bit.ly/atp2020-live

    View Slide

  11. http://bit.ly/atp2020-codelabs

    View Slide

  12. http://bit.ly/kahoot-aula4

    View Slide

  13. Resumo da Aula #3

    View Slide

  14. Layouts
    +351990000001

    View Slide

  15. Views
    +351990000001

    View Slide

  16. Resolução
    medium resolution
    (mdpi)
    HTC Wildfire S
    high resolution
    (hdpi)
    Samsung Galaxy S2
    extra extra extra high resolution
    (xxxhdpi)
    Pixel 5

    View Slide

  17. +351990000001

    View Slide

  18. Trabalho para casa
    ● Melhora a UI do ecrã inicial
    ● Adicionar uma ScrollView
    ● Enviar o nome de utilizador para a Activity principal
    ● Utilizar o botão de ação do teclado para validar

    View Slide

  19. Aula #4

    View Slide


  20. e… o que acontece quando
    rodamos o ecrã?

    View Slide


  21. e… o que acontece quando
    rodamos o ecrã?

    View Slide

  22. View Slide

  23. View Slide

  24. Architecture
    components

    View Slide

  25. LiveData
    ViewModel
    Room

    View Slide

  26. ViewModel

    View Slide

  27. ViewModel
    A classe ViewModel foi desenhada para guardar e gerir dados relacionados com
    a interface do utilizador de forma consciente em relação ao ciclo de vida.
    Esta classe permite que os dados continuem disponíveis mesmo quando as
    configurações são alteradas. Por exemplo, quando o ecrã é rodado e a activity
    é reconstruída.
    - Android Developers documentation

    View Slide

  28. ● Permite separar os dados da sua representação gráfica
    ● Está associado a uma Activity ou Fragment
    ● Continua a ser executado mesmo que a aplicação não esteja visível
    ● Permanece em memória quando uma Activity é reconstruída
    ● Não deve aceder à interface gráfica da aplicação
    ViewModel

    View Slide

  29. Ciclo de vida
    Activity onCreate
    onResume
    onPause
    onDestroy
    ecrã rodado
    onCreate
    onResume
    activity recriada
    finish()
    onPause
    onDestroy
    activity destruída
    ViewModel
    onCleared()
    Activity
    activity criada

    View Slide

  30. dependencies {
    ...
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    }
    Como utilizar?
    Importar a biblioteca
    app/build.gradle

    View Slide

  31. class CounterViewModel : ViewModel() {
    }
    Como utilizar?
    Implementar numa class
    CounterViewModel.kt

    View Slide

  32. class CounterViewModel : ViewModel() {
    var number: Int = 0
    private set
    fun increment() {
    ++number
    }
    }
    Como utilizar?
    Lógica no ViewModel
    CounterViewModel.kt

    View Slide

  33. class CounterViewModel : ViewModel() {
    var number: Int = 0
    private set
    fun increment() {
    ++number
    }
    }
    Como utilizar?
    Lógica no ViewModel
    não é possível alterar o
    valor de number, fora desta
    class
    CounterViewModel.kt

    View Slide

  34. class CounterActivity : AppCompatActivity() {
    lateinit var viewModel : CounterViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_counter)
    viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
    }
    }
    Como utilizar?
    Instanciar numa Activity
    CounterActivity.kt

    View Slide

  35. class CounterActivity : AppCompatActivity() {
    lateinit var viewModel : CounterViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_counter)
    viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
    }
    }
    Como utilizar?
    Instanciar numa Activity
    CounterActivity.kt

    View Slide

  36. class CounterActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    findViewById(R.id.btn_counter).setOnClickListener {
    viewModel.increment()
    Toast.makeText(this, "Number: ${viewModel.number}", LENGTH_SHORT).show()
    }
    }
    }
    Como utilizar?
    Lógica na Activity
    CounterActivity.kt

    View Slide

  37. View Slide

  38. class CounterActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val tvCounter = findViewById(R.id.tv_counter)
    findViewById(R.id.btn_counter).setOnClickListener {
    viewModel.increment()
    tvCounter.text = viewModel.number.toString()
    }
    }
    }
    CounterActivity.kt
    Como utilizar?
    Actualizar uma TextView

    View Slide

  39. View Slide

  40. LiveData

    View Slide

  41. class CounterViewModel : ViewModel() {
    var number: Int = 0
    private set
    fun increment() {
    ++number
    lotsOfProcessings(number)
    }
    ...
    }
    Bué processamento

    View Slide

  42. View Slide

  43. View Slide

  44. ● Podemos ter um ViewModel partilhado,
    ○ Permite comunicar entre uma Activity e um Fragment
    ○ Permite comunicar entre vários Fragment’s
    E ainda...

    View Slide

  45. LiveData

    View Slide

  46. LiveData
    O LiveData é uma classe que contém dados
    observáveis. Ao contrário de uma classe observável
    normal, o LiveData tem em consideração o ciclo de
    vida, ou seja, respeita o ciclo dos componentes como
    Activities, Fragments ou Service’s. Esta
    consideração assegura que o LiveData só atualiza o
    componente que estão num estado ativo.
    - Android Developers documentation

    View Slide

  47. LiveData
    O LiveData é uma classe que contém dados
    observáveis. Ao contrário de uma classe observável
    normal, o LiveData tem em consideração o ciclo de
    vida, ou seja, respeita o ciclo dos componentes como
    Activities, Fragments ou Service’s. Esta
    consideração assegura que o LiveData só atualiza o
    componente que estão num estado ativo.
    - Android Developers documentation

    View Slide

  48. ● Padrão de desenvolvimento de software
    ● Um objeto (Observable) notifica os interessados (Observers)
    ○ Quando este valor é alterado
    ● Os interessados subscrevem para receber as atualizações
    Observável?
    Observable
    Observer
    Observer
    Observer
    ...

    View Slide

  49. ● Assegura que a interface gráfica corresponde ao estado dos dados
    ● Não tem memory leaks
    ● Sem crashes por causa de Activities paradas
    ● Sem manipulação manual do ciclo de vida
    ● Sempre atualizado
    ● Partilha de recursos
    Vantagens do LiveData

    View Slide

  50. dependencies {
    ...
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    }
    Como utilizar?
    Importar a biblioteca
    app/build.gradle

    View Slide

  51. class CounterViewModel : ViewModel() {
    var number: Int = 0
    private set
    fun increment() {
    ++number
    }
    }
    Como utilizar?

    View Slide

  52. class CounterViewModel : ViewModel() {
    private val _liveNumber = MutableLiveData()
    private var number: Int = 0
    val liveNumber : LiveData = _liveNumber
    fun increment() {
    ++number
    _liveNumber.postValue(number)
    }
    }
    Como utilizar?
    No ViewModel

    View Slide

  53. class CounterViewModel : ViewModel() {
    private val _liveNumber = MutableLiveData()
    private var number: Int = 0
    val liveNumber : LiveData = _liveNumber
    fun increment() {
    ++number
    _liveNumber.postValue(number)
    }
    }
    Como utilizar?
    No ViewModel

    View Slide

  54. class CounterViewModel : ViewModel() {
    private val _liveNumber = MutableLiveData()
    private var number: Int = 0
    val liveNumber : LiveData = _liveNumber
    fun increment() {
    ++number
    _liveNumber.postValue(number)
    }
    }
    Como utilizar?
    No ViewModel

    View Slide

  55. class CounterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
    val tvCounter = findViewById(R.id.tv_counter)
    findViewById(R.id.btn_counter).setOnClickListener {
    viewModel.increment()
    }
    viewModel.liveNumber.observe(this){ number ->
    tvCounter.text = number.toString()
    }
    }
    }
    Como utilizar?
    Na Activity

    View Slide

  56. class CounterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
    val tvCounter = findViewById(R.id.tv_counter)
    findViewById(R.id.btn_counter).setOnClickListener {
    viewModel.increment()
    }
    viewModel.liveNumber.observe(this){ number ->
    tvCounter.text = number.toString()
    }
    }
    }
    Como utilizar?
    Na Activity

    View Slide

  57. class CounterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
    val tvCounter = findViewById(R.id.tv_counter)
    findViewById(R.id.btn_counter).setOnClickListener {
    viewModel.increment()
    }
    viewModel.liveNumber.observe(this){ number ->
    tvCounter.text = number.toString()
    }
    }
    }
    Como utilizar?
    Na Activity

    View Slide

  58. LiveData

    View Slide

  59. ● LiveData
    ○ É uma classe abstrata, não permite atualizar o valor
    ● MutableLiveData
    ○ Classe que permite alterar o valor
    ● Transformations.map
    ○ Permite transformar o item
    ● MediatorLiveData
    ○ Permite juntar dois LiveData
    ● Transformations.SwitchMap
    ○ Utiliza o valor de um LiveData para produzir outro
    Tipos de LiveData

    View Slide

  60. Operações
    assíncronas

    View Slide

  61. atualizar atualizar atualizar atualizar
    desenha o ecrã desenha o ecrã desenha o ecrã
    objetivo: 60fps
    UI-thread
    Operações assíncronas

    View Slide

  62. atualizar operação pesada atualizar
    desenha o ecrã desenha o ecrã desenha o ecrã
    objetivo: 60fps
    UI-thread
    Operações assíncronas

    View Slide

  63. ● A aplicação não é fluida
    ○ Alguns frames podem não ser desenhados
    ● ANR podem ser lançados pelo sistema
    ○ ANR = Activity Not Responding
    ● Má experiência para o utilizador
    Operações assíncronas
    Counter isn’t responding

    View Slide

  64. atualizar atualizar
    desenha o ecrã desenha o ecrã desenha o ecrã
    objetivo: 60fps
    operação pesada
    atualizar atualizar
    thread secundária
    Operações assíncronas
    thread de UI

    View Slide

  65. atualizar atualizar
    desenha o ecrã desenha o ecrã desenha o ecrã
    objetivo: 60fps
    thread de UI
    operação pesada
    atualizar atualizar
    Operações assíncronas
    progresso
    thread secundária

    View Slide

  66. ● A aplicação fica muito mais fluida
    ○ Não há perda de frames
    ● Operações pesadas feitas numa thread secundária
    ● Atualizações de UI são feitas na thread de UI
    ○ UI = User Interface
    ● O ecrã é atualizado cada 16 ms
    Operações assíncronas

    View Slide

  67. ● Service
    ● Threads
    ● IntentService
    ● AsyncTasks
    ● WorkManager
    ● JobScheduler
    ● DownloadManager
    ● AlarmManager
    ● Coroutines
    Operações assíncronas
    Soluções

    View Slide

  68. ● Service
    ● Threads
    ● IntentService
    ● AsyncTasks
    ● WorkManager
    ● JobScheduler
    ● DownloadManager
    ● AlarmManager
    ● Coroutines
    Operações assíncronas
    Soluções

    View Slide

  69. … e qual é que devemos
    mesmo escolher?

    View Slide

  70. Operações assíncronas
    Soluções
    executam num
    momento exato
    downloads via
    HTTP
    requer a
    atenção do
    utilizador
    resultam de
    eventos do
    sistema
    DownloadManager
    ForegroundService
    WorkManager
    AlarmManager
    descarregar os
    materiais desta
    aula
    atualizar a caixa de
    entrada de emails
    ligar a uma rede
    WiFi
    o alarme a tocar
    que já são horas de
    acordar

    View Slide

  71. Permissões

    View Slide

  72. Permissões manuais Transparência
    Limite de permissões
    Tipo de permissões
    Segurança

    View Slide

  73. Ao instalar todas as
    permissões requisitadas
    são dadas
    Android ...

    View Slide

  74. Ao instalar todas as
    permissões requisitadas
    são dadas
    Android ...

    View Slide

  75. Allow Snapchat to access
    this device’s location?
    Ao instalar todas as
    permissões requisitadas
    são dadas
    Funcionalidades do sistemas
    precisam de autorização
    explícita do utilizador
    Android ... Android 6.0

    View Slide

  76. Allow Snapchat to access
    this device’s location?
    Ao instalar todas as
    permissões requisitadas
    são dadas
    Funcionalidades do sistemas
    precisam de autorização
    explícita do utilizador
    As permissões vão sendo cada vez mais restritivas:
    - Apenas enquanto estamos a utilizar a aplicação
    - Permitir apenas uma única vez
    Android ... Android 6.0 … Android 10

    View Slide

  77. Allow Snapchat to access
    this device’s location?
    Ao instalar todas as
    permissões requisitadas
    são dadas
    Funcionalidades do sistemas
    precisam de autorização
    explícita do utilizador
    As permissões vão sendo cada vez mais restritivas:
    - Apenas enquanto estamos a utilizar a aplicação
    - Permitir apenas uma única vez
    Android ... Android 6.0 … Android 10 Android 11

    View Slide

  78. Android 11
    ● Permissão única
    ● O utilizador tem de
    permitir o acesso
    contínuo à localização
    ○ Maior segurança
    ○ Poupança de bateria
    ● Revogação de permissões
    para aplicações que não
    são utilizadas

    View Slide

  79. Android 10+
    ● Permissão de leitura/escrita de ficheiros não é
    suficiente.
    ● Ficheiro não criados pela aplicação que sejam
    modificados precisam de permissão adicional.

    View Slide

  80. val uri = MediaStore.setRequireOriginal(imageUri)
    contentResolver.openInputStream(uri).use { inputStream ->
    // ...
    }
    Aceder a um ficheiro

    View Slide

  81. val uri = MediaStore.setRequireOriginal(imageUri)
    contentResolver.openInputStream(uri).use { inputStream ->
    // ...
    }
    Aceder a um ficheiro
    E/AndroidRuntime: FATAL EXCEPTION: main
    Process: pt.atp.bobi, PID: 30411
    java.lang.RuntimeException: Unable to start activity
    ComponentInfo{pt.atp.bobi/pt.atp.toy.CounterActivity}: java.io.FileNotFoundException:
    /storage/emulated/0/bobi.mp4: open failed: EACCES (Permission denied)

    View Slide

  82. val uri = MediaStore.setRequireOriginal(imageUri)
    contentResolver.openInputStream(uri).use { inputStream ->
    // ...
    }
    Aceder a um ficheiro
    E/AndroidRuntime: FATAL EXCEPTION: main
    Process: pt.atp.bobi, PID: 30411
    java.lang.RuntimeException: Unable to start activity
    ComponentInfo{pt.atp.bobi/pt.atp.toy.CounterActivity}: java.io.FileNotFoundException:
    /storage/emulated/0/bobi.mp4: open failed: EACCES (Permission denied)

    View Slide

  83. if(ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED) {
    // Não temos permissão ✋
    }
    Como verificar?

    View Slide

  84. if(ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED)
    {
    ActivityCompat.requestPermissions(this,
    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
    REQUEST_CODE)
    }
    Como pedir?

    View Slide

  85. if(ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED)
    {
    ActivityCompat.requestPermissions(this,
    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
    REQUEST_CODE)
    }
    Como pedir?

    View Slide

  86. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array,
    grantResults: IntArray) {
    if (requestCode == REQUEST_CODE) {
    if (grantResults.isNotEmpty() &&
    grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    // Acesso a tudo
    } else {
    // Ainda não dá ✋
    }
    } else {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
    }
    Resultado

    View Slide

  87. if(ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED)
    {
    ActivityCompat.requestPermissions(this,
    arrayOf(
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.CAMERA
    ),
    REQUEST_CODE)
    }
    Várias permissões

    View Slide





  88. E ainda...

    View Slide

  89. ● Pedir a permissão apenas quando o utilizador começa a interagir com a
    funcionalidade que necessita dessa permissão.
    ● Não bloquear o utilizador. Dar a possibilidade ao utilizador de cancelar a
    permissão.
    ● Se o utilizador rejeitar ou revogar a permissão que uma funcionalidade
    necessita, devemos permitir que o utilizador continue a utilizar a aplicação,
    possivelmente até a desativar a funcionalidade em questão.
    Algumas recomendações

    View Slide

  90. Passaporte para
    a Google

    View Slide

  91. Isabel Póvoa
    Strategic Cloud Engineer
    Google

    View Slide

  92. Abre o Android
    Studio e vamos
    começar a
    programar
    ‍‍

    View Slide

  93. VS
    Ronda 4

    View Slide

  94. class kennel {
    val dog by lazy {
    Dog()
    }
    }
    Instanciação Lazy
    class Kennel {
    private Dog dog;
    public Dog getDog() {
    if(dog == null){
    dog = new Dog();
    }
    return dog;
    }
    }

    View Slide

  95. class Dog(val name:String,
    val color: String,
    val legs: int)
    val dog = Dog("Bobi", "Verde", 5)
    val (name, color, legs) = dog
    println(name) // Bobi
    println(color) // Verde
    println(legs) // 5
    ‍♂
    Deconstrução de uma variável

    View Slide

  96. val (name, color, legs) = dog
    if(legs > 4) {
    println("O $name, é um cão? ")
    }
    ‍♂
    Deconstrução de uma variável

    View Slide

  97. val (name, _, legs) = dog
    if(legs > 4) {
    println("O $name, é um cão? ")
    }
    ‍♂
    Deconstrução de uma variável

    View Slide

  98. val dogs = listOf(...)
    for ((name, color, legs) in dogs) {
    // ...
    }
    ‍♂
    Deconstrução de uma variável

    View Slide

  99. fun `O Bobi é lindo`() {
    ...
    }
    Espaços em nomes de funções ‍♂
    ‍♂

    View Slide

  100. @Test
    fun `O Bobi é lindo`() {
    ...
    }
    Espaços em nomes de funções ‍♂
    ‍♂
    * Apenas aceitável utilizar isto em testes

    View Slide

  101. Sexta-Feira
    negra
    Photo by Xiaolong Wong on Unsplash
    Produto não disponível
    (voltamos para a semana)

    View Slide

  102. Castanhas quentes e
    boas!
    Photo by sare . on Unsplash

    View Slide

  103. Destruir todas as Activities

    Para ativar:
    1. Definições
    2. Sistema
    3. Opções de programador
    4. Não manter atividades

    View Slide

  104. Destruir todas as Activities

    Para ativar:
    1. Definições
    2. Sistema
    3. Opções de programador
    4. Não manter atividades

    View Slide

  105. Android Studio

    Para criar uma pasta:
    1. Botão direito na barra lateral
    2. “New Project Group”
    Para mover um projeto:
    1. Botão direito na barra lateral
    2. “Move To Group”

    View Slide

  106. Android Studio

    Modificar as cores do logcat
    1. Android Studio (barra no topo)
    2. Preferences
    3. Pesquisar por logcat
    4. Selecionar o tipo e a cor

    View Slide

  107. adb

    Enviar texto para o telemóvel/emulador
    1. Abrir o Terminal/Linha de comandos
    2. adb shell input text “Isto é uma mensagem de texto com um emoji ”
    http://bit.ly/atp2020-codelabs

    View Slide

  108. adb

    Enviar texto para o telemóvel/emulador
    1. Abrir o Terminal/Linha de comandos
    2. adb shell input text “Isto é uma mensagem de texto com um emoji ”
    http://bit.ly/atp2020-codelabs

    View Slide

  109. Trabalho Para Casa
    ‍‍

    View Slide

  110. Trabalho para casa
    ● Adicionar ViewModel
    ○ Login
    ○ MainActivity
    ● Alterar a imagem, pedindo permissão

    View Slide

  111. Dúvidas?

    View Slide

  112. Continuamos a
    responder no
    discord

    View Slide

  113. Obrigado
    ‍♀

    View Slide

  114. Android
    Training
    Program
    PORTUGAL
    Aula #5
    Listas, listas e mais listas
    Próxima aula: 18 de Novembro

    View Slide