DS151 - Desenvolvimento Mobile
Tecnologia em Análise e Desenvolvimento de Sistemas
Setor de Educação Profissional e Tecnológica - SEPT
Universidade Federal do Paraná - UFPR
Prof. Alexander Robert Kutzke
Material da disciplina DS151 - Desenvolvimento Mobile.
Aula 01 - Introdução
Plano de ensino e avaliação
Consultar moodle da disciplina (https://www.ufprvirtual.ufpr.br).
O que aprenderemos?
Como desenvolver aplicativos nativos para Android utilizando Kotlin e Android Studio.
Como aprenderemos?
Por meio de aulas teóricas e práticas. Aulas práticas que serão compostas por um exercício a ser entregue ao professor na própria aula.
Todas as entregas de exercícios serão feitas pelo moodle e utilizando links de repositórios criados pelos alunos no gitlab do curso (gitlab.com).
Essa disciplina é difícil?
Não. Porém o bom aproveitamento depende de muita prática. O aprendizado do desenvolvimento de aplicativos para Android envolve, entre outras coisas, o estudo (e o uso) de vários exemplos de código prontos. Isso facilita o processo de desenvolvimento, uma vez que boa parte do trabalho acaba se tornando a ligação de pedaços de código já funcionais. Mas, após um certo amadurecimento do conhecimento é necessário que a compreensão de como esses trechos de códigos são formados seja expandida.
Portanto, é necessário praticar para que os exemplos sejam apreendidos e aprendidos de forma adequada. Não se assuste com a quantidade de códigos novos no início da disciplina. A ideia é que eles se tornem, pouco a pouco, mais "palatáveis". Além disso, utilizaremos uma linguagem nova - o Kotlin.
Android
- Sistema Operacional para dispositivos móveis mais utilizado em todo o mundo:
- Mais de 2bi de dispositivos ativos;
- Além do sistema operacional, inclui:
- Middleware (comunicação entre aplicativos);
- Aplicações-chave (telefone, câmera, etc);
- Kernel do Linux como base;
- Open-source ( http://source.android.com ):
- Licença Apache;
- Cada fabricante pode criar seu Android "personalizado";
- Entretanto, para ter Apps do Google, o sistema deve ser homologado;
- Primeira versão lançada em 2008;
- API Level é um número sequencial que identifica a versão do Android (cada versão possui o nome de um doce);
- Estamos indo para a API Level 36, versão 16:
- Para saber mais: https://en.wikipedia.org/wiki/Android_version_history
- É importante conhecer as versões do Android para sabermos quais as APIs, classes e recursos estão disponíveis para nossos aplicativos em certos aparelhos;
Android Studio
- O Android Studio é o IDE (Integrated Development Environment) padrão para desenvolvimento de aplicações Android;
- Uma personalização do IntelliJ IDEA Community, criado pela Jetbrains;
- Traz MUITAS ferramentas para auxiliar no processo de desenvolvimento;
Download
- Todos deverão ter instalado em seus computadores (ou utilizar o lab);
- Para isso, basta fazer o download do pacote específico para o seu sistema operacional:
https://developer.android.com/studio/
- A versão utilizada nesse material é a
Android Studio Meerkat | 2024.3.1
; - O pacote baixado já traz todas as dependências necessárias:
- OpenJDK;
- Gradle;
- Android Debug Bridge (adb);
- SDK Manager;
- Após instalado, ao iniciar o Android Studio, provavelmente a seguinte tela aparecerá:
- A partir dessa tela, é possível acessar o SDK Manager (Configure -> SDK Manager);
- Nele, é possível baixar e instalar componentes do SDK:
- Na aba SDK Platforms, clique em Show Package Details;
- Marque, pelo menos, os seguintes itens:
- Android 14.0 (UpsideDownCake) - ou o que funcionar nos computadores do lab:
- Android SDK Platform 34;
- Sources for Android 34;
- Android 14.0 (UpsideDownCake) - ou o que funcionar nos computadores do lab:
- Na aba SDK Tools adicione, ao menos, os itens:
- Android SDK Build-Tools;
- Android Emulator;
- Android SDK Platform-Tools;
- Android SDK Command-line Tools;
- Por fim, clique em Apply para finalizar;
- Aguarde o download e a instalação.
Kotlin
- https://kotlinlang.org/
- Kotlin é uma Linguagem de programação que roda em uma Máquina virtual Java e que também pode ser traduzida para JavaScript;
- É a linguagem oficial para o desenvolvimento de aplicativos para Android;
- Tem interoperabilidade completa com Java;
- Aprenderemos no decorrer da disciplina:
- Mas, para iniciarmos, assista as aulas do curso da Udemy (feito pelo Google)
até, pelo menos, o tópico de Funções:
- https://www.udacity.com/course/kotlin-bootcamp-for-programmers--ud9011
- Possui legendas em português.
- Mas, para iniciarmos, assista as aulas do curso da Udemy (feito pelo Google)
até, pelo menos, o tópico de Funções:
Mais recursos
- Developer Guides Official: https://developer.android.com/guide
Referências
- Google, Developer Guides https://developer.android.com/guide acessado em 12/03/2025.
- Glauber, Nelson, Dominando o Android com Kotlin, São Paulo : Novatec, 2019, 3ed.
Aula 02 - Conceitos Básicos
A aula tem como objetivo criar um primeiro aplicativo Android e analisar sua estrutura principal.
Iniciando um novo projeto com Android Studio
-
Para criar um novo aplicativo no Android Studio:
-
Em sua tela inicial, selecione a opção Start a new Android Studio project ou File > New > New Project;
-
A tela acima permite a escolha de um template para a Activity:
- Activity é a classe responsável por criar uma tela na aplicação Android;
- A activity inicial de uma aplicação é comumente nomeada MainActivity;
- Toda activity tem seu conteúdo visual definido em arquivos XML, conhecidos como "Arquivos de layout":
- Mais recentemente, o Jetpack Compose permite formas mais simples de criação de layouts;
-
Selecione a opção Empty Activity para um primeiro exemplo;
-
Na sequência é necessário acrescentar as primeiras configurações da aplicação. São elas:
- Name: nome que aparecerá no aparelho;
- Package Name: nome do pacote. Este nome deve ser único!
- Save location: onde o projeto será salvo no computador;
- Minimum API Level: qual a versão mínima do Android que o aparelho deve possuir para executar a aplicação. Procuraremos sempre utilizar, no mínimo, a API Level 24 (Android Nougat);
- Build configuration language: define scripts de build padrão para Kotlin;
-
-
Após criar o novo projeto, o Android Studio irá realizar verificações de dependências necessárias para realização do build do projeto. Espere pacientemente. Isso pode envolver alguns downloads:
-
Além disso, ele pode iniciar o processo de "Indexing";
-
Esse processo é o reconhecimento dos arquivos do projeto para que o Android Studio consiga executar todas as suas funcionalidades;
-
Novamente, aguarde pacientemente. :)
-
Android Jetpack
- O Android Jetpack é uma coleção de bibliotecas para facilitar o desenvolvimento de aplicativos Android;
- Auxilia no uso das práticas recomendadas de desenvolvimento;
- Simplifica tarefas complexas;
- Tem desenvolvimento separado da API Level:
- Significa que tem updates mais frequentes;
- Não é necessário aguardar uma nova versão do Android para acessar novas funcionalidades;
- Antes do Jetpack, não havia padrão para os nomes de bibliotecas oficiais do Android:
android.arch.*
,com.android.support.*
,android.databinding.*
...;- Com o Jetpack, todas as bibliotecas estão no pacote
androidx.*
; - Como esse é um processo em transição, ainda existem vários projetos de aplicativos que utilizam as bibliotecas antigas;
- O objetivo é que todos migrem para o Jetpack:
- O próprio Android Studio possui uma opção para migrar projetos para o Jetpack (Refactor > Migrate to Android X);
- Recentemente o Jetpack Compose foi lançado2:
- Forma mais simples de gerar layouts para aplicações;
- Substitui os arquivos
horríveisde layout XML;
- Mais informações: https://developer.android.com/jetpack/
Estrutura de um projeto Android
-
No canto superior esquerdo do Android Studio é possível selecionar diferentes visualizações da estrutura do projeto;
-
Selecione o tipo Project;
-
Você verá uma organização semelhante a essa:
-
Analisaremos detalhes dessa estrutura a seguir.
AndroidManifest.xml
- O arquivo AndroidManifest.xml serve como um arquivo central de configuração do aplicativo;
- Os dados utilizados na tela de configuração do aplicativo estão aqui:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HelloWorldTADS"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.HelloWorldTADS">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
- Todas as activities que serão exibidas para os usuários deverão estar declaradas no AndroidManifest.xml;
- Note que a action
android.intent.action.MAIN
indica qual activity é o ponto de entrada da aplicação:- Ou seja, qual activity vai ser iniciada quando o usuário abrir o aplicativo;
- A category
android.intent.category.LAUNCHER
indica que essa activity deverá aparecer no menu de aplicações do aparelho. - Valores que possuem o símbolo "@" indicam um recurso da aplicação.
Arquivos de Recursos
-
Os recursos da aplicação são imagens, textos, layouts, e outros itens a serem utilizados pelo desenvolvedor;
-
Todos eles ficam localizados em
app/src/main/res
; -
Para cada recurso, um identificador é mapeado automaticamente;
-
Esse identificador fica armazenado na classe
R
; -
Por exemplo:
- Na pasta
res/mipmap-mdpi
, existe um arquivo chamadoic_launcher.webp
; - É possível acessar esse arquivo pelo identificador
R.mipmap.ic_launcher
em qualquer código Kotlin da aplicação:- Nos arquivos xml, ao invés de utilizarmos a classe R, utilizamos o sinal
@
para referenciarmos algum recurso;
- Nos arquivos xml, ao invés de utilizarmos a classe R, utilizamos o sinal
- Vejamos mais uma vez um trecho do AndroidManifest.xml:
android:label="@string/app_name"
- Na definição acima, a propriedade
android:label
está utilizando como valor o recurso@string/app_name
. Ou seja, o arquivores/values/strings.xml
será consultado; - Dentro desse arquivo, a string nomeada
app_name
trará o valor do recurso; - Ao consultar o arquivo
res/values/strings.xml
temos:
<resources> <string name="app_name">Hello World TADS</string> </resources>
- Em resumo, para acessar o valor da string
app_name
:- No kotlin:
getString(R.string.app_name)
; - No xml:
@string/app_name
;
- No kotlin:
- Alguns mapeamentos de recursos comuns são mostrados a seguir:
- Na pasta
Recurso | ID da Classe R | Arquivo XML |
---|---|---|
res/mipmap/ic launcher.png | R.mipmap.ic_launcher | @mipmap/ic_launcher |
res/drawable/imagem.png | R.drawable.imagem | @drawable/imagem |
res/layout/activity_main.xml | R.layout.activity_main | @layout/activity_main |
res/menu/menu_main.xml | R.menu.menu main | @menu/menu_main |
res/values/strings.xml <string name="ola"> | R.string.ola | @string/ola |
- O sistema de identificação dos recursos na aplicação é mais esperto do que parece. Mais a frente veremos alguns detalhes interessantes.
MainActivity.kt
- Nosso primeiro código kotlin:
package com.example.helloworldtads
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.helloworldtads.ui.theme.HelloWorldTADSTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
HelloWorldTADSTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
HelloWorldTADSTheme {
Greeting("Android")
}
}
-
Diferenças claras com relação ao Java:
-
Extensão do arquivo (.kt);
-
Sem
;
no final do comandos (é opcional); -
Operador de herança
:
:- Classe
MainActivity
está herdandoComponentActivity
;
- Classe
-
Toda classe herda de
Any
e não deObject
; -
Ao sobreescrever um método, é necessário utilizar a palavra-chave
override
; -
Toda função (ou método) tem a seguinte sintaxe:
-
<modificador> fun nomeDaFuncao(<parametros>) : <tipo de retorno>
:private fun soma(a:Int, b:Int): Int { return a + b }
-
-
Não existem tipos primitivos. Todos são classes;
-
Possui o recurso de nulabilidade (nullability):
-
É obrigatório declarar se um atributo, variável ou parâmetro pode ser nulo ou não;
-
Se sim, adiciona-se um
?
ao tipo do item. Ex.:Int?
; -
Outros exemplos:
var a:String = "Kotlin" a = null // não compila! "a" não aceita nulo var b:String? = "abc" b = null // ok val lenA = a.length // ok! é garantido que "a" não é nulo val lenB = b.length // não compila, pois "b" pode ser nulo val lenB1 = if (b != null) b.length else -1 val lenB2 = b?.length ?: -1 // "Elvis operator"
-
var
indica variável, enquantoval
indica constante; -
Possuí inferência automática de tipos;
-
Estrutura if/else sempre retorna um valor;
-
O Elvis Operator (
?:
) serve para verificar se determinada expressão é nula e, caso positivo, retorna um valor não nulo.
-
-
-
Sobre o código da MainActivity:
- O método
onCreate(Bundle)
é chamado quando a Activity é criada e nele é chamado o métodosetContent
para informar qual é o componente a ser desenhado na tela;
- O método
Arquivo de Layout (como era com arquivos XML)
-
Os arquivos de layout são arquivos XML que definem a estrutura visual de uma ou mais activities;
-
Localizam-se na pasta
res/layout
; -
Na MainActivity, utilizamos a classe
R
para referenciar um layout (R.layout.activity_main
); -
Ao abrir o arquivo de layout correspondente, temos acesso ao editor de layouts:
-
Este pode ser utilizado com a interface gráfica (Design) ou de texto;
-
O layout da nossa MainActivity possui o seguinte conteúdo xml:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
É possível observar pelo arquivo acima que nosso layout é composto por:
- Um gerenciador de layout conhecido por
ConstraintLayout
; - Um
<TextView>
com conteúdo "Hello World!" e algumas indicações de posicionamento;
- Um gerenciador de layout conhecido por
-
Configuração do Aparelho
-
Para executar nossa aplicação em um aparelho real é necessário, primeiramente, configurar o aparelho:
-
Acesse o menu Sobre o telefone nas configurações do aparelho (provavelmente no menu Sistema);
-
Toque 7 vezes no item Número da Versão (ou Build Number ou qualquer outro nome parecido com isso);
-
Você verá o aviso "Você agora é um desenvolvedor";
-
Isso habilitará o menu Opções de desenvolvedor nas configurações do aparelho;
-
Nesse menu, ative a opção Depuração USB (USB Debugging);
-
Agora ao conectar o aparelho ao computador com um cabo USB, o Android Studio será capaz de reconhecê-lo:
- Pode ser que uma mensagem de confirmação apareça no aparelho. Apenas confirme;
- Normalmente, em dispositivos Unix, o aparelho é reconhecido automaticamente. No windows, às vezes, é necessário baixar o driver do aparelho no site do fabricante.
-
Android Virtual Device
- Um Android Virtual Device (AVD) é um emulador do Android;
- Pode-se utilizar um AVD para testar aplicativos do Android Studio sem a necessidade de um aparelho real;
- Uma das vantagens do AVD é a possibilidade de testar o aplicativo em diferentes tipos de dispositivos (tamanho de tela, resolução, recursos, etc);
- Para computadores com processador Intel é possível instalar o HAXM (Hardware Accelerated Execution Manager):
- Nos casos em que há compatibilidade, o Android Studio já instala esse recurso automaticamente;
- Para criar um AVD, basta acessar o menu Tools > AVD Manager ou Tools > Device Manager no Android Studio e seguir as instruções;
- Em um dado momento da configuração, será necessário selecionar uma imagem de sistema. Ou seja, qual a versão do Android que o emulador irá utilizar:
- Nesse momento, o download da imagem deverá ser realizado.
- Existem outros emuladores de Android que não são oficiais da empresa Google.
Executando a Aplicação
-
Uma vez você tenha um celular conectado ao seu computador pela porta USB ou um AVD pronto, o Android Studio irá reconhecer esse aparelho como um alvo para execução da sua aplicação:
-
Nesse momento, basta clicar no botão Run app (botão verde em formato de play);
-
Ao fazer isso, sua aplicação será compilada e instalada no dispositivo, e inicializada automaticamente:
Processo de Compilação
- O Android Studio utiliza o Gradle (https://www.gradle.org) como ferramenta para gerenciamento do build dos aplicativos;
- Essa ferramenta é responsável por toda a configuração e inclusão/download de dependências para o processo de compilação do aplicativo;
- O Gradle é uma aplicação completa, independente do Android Studio:
- Sua interface mais comumente utilizada é pela linha de comando;
- Também é bastante utilizado em projetos de outras linguagens;
- Em um projeto, o arquivo principal para o Gradle é nomeado
build.gradle
; - Nos projetos do Android Studio, existem, pelo menos, dois arquivos com esse nome: um na raiz do projeto e outro em cada módulo (o módulo que vamos ver nesse curso trata do aplicativo smartphone e tablets, comumente nomeado de app);
- O arquivo
build.gradle
do nosso módulo app contém algo semelhante a isso:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.helloworldtads"
compileSdk = 35
defaultConfig {
applicationId = "com.example.helloworldtads"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
- O Gradle funciona essencialmente a base de plugins;
- Nas 3 primeiras linhas do arquivo acima, são invocados 3 plugins do gradle: uma para desenvolvimento para android e dois referentes ao uso do kotlin;
- Esses plugins possuem as informações necessárias para que a compilação seja adequada às necessidades do projeto (Android + Kotlin);
- No bloco
android
residem as configurações específicas para o plugin android; - Ao final do arquivo, o bloco
dependencies
define as dependências de bibliotecas externas ou módulos necessários pelo projeto:- Cada dependência pode ter a versão específica no final de sua linha;
- O arquivo
build.gradle
da raiz do projeto possui definições que serão utilizadas em todos os módulos:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
- No decorrer das aulas trataremos de mais detalhes sobre o Gradle.
Alocação dinâmica de recursos
-
Todos os recursos de uma aplicação (strings, imagens, estilos, layouts, etc.) devem, obrigatoriamente, estar na pasta
res/
; -
Cada subdiretório dessa pasta tem, entretanto, objetivos específicos. Os principais são:
- anim/ e animator/: arquivos XML de animações quadro-a-quadro ou de efeito;
- drawable/: arquivos de imagens da aplicação .jpg ou.png, ou ainda arquivos XML que descrevem algo que será desenhado na tela;
- font/: arquivos de fontes .ttf ou.otf, ou arquivos XML que descrevem famílias de fontes;
- layout/: arquivos XML com a definição do conteúdo visual das telas;
- menu/: arquivos XML com as opções de menu;
- mipmap/: ícone da aplicação;
- raw/: arquivos binários diversos que podem ser usados no projeto;
- transition/ arquivos XML para descrever animações de transição;
- values/: arquivos XML com valores tais como: strings (texto simples); string_arrays (lista de valores), dimensions (definição de tamanhos); colors (definição de cores) e styles (estilos);
- xml/: essa pasta normalmente armazena arquivos XML de metadados da aplicação;
-
Todos os arquivos de recursos devem ser nomeados com letras minúsculas e não devem conter símbolos (exceto
_
):- A partir do segundo caractere podem conter números;
-
A extensão do arquivo não é utilizada para a geração do id da classe R, portanto não é possível ter um mesmo arquivo com duas extensões diferentes;
-
O Android possui um recurso de escolha dinâmica de recursos (ou alocação dinâmica) em que o sistema operacional seleciona o recurso mais apropriado de acordo com as configurações do aparelho:
-
Para isso, é preciso apenas criar variações nos diretórios de recursos adicionando sufixos especiais aos seus nomes:
-
-
É possível combinarmos sufixos. Por exemplo:
layout-pr-rBR-large
: layouts a serem utilizados em aparelhos configurados em português brasileiro e com uma tela grande;values-mcc724-mnc31-v21
: valores a serem utilizados em aparelhos no Brasil, da operadora Oi e com API level 21;drawable-land-hdpi
: desenhos a serem utilizados em aparelhos em posição horizontal e com uma densidade de tela de 240dpi;
-
Além disso o Android tentará, na ausência de uma pasta específica, fazer a melhor aproximação possível do recurso a ser utilizado.
Idioma
-
Como exemplo, podemos realizar um processo de internacionalização do nosso aplicativo inicial;
-
Abra o arquivo
res/values/strings.xml
e adicione o seguinte:<resources> <string name="app_name">Hello World TADS</string> <string name="test">This is a test</string> </resources>
-
Agora, no arquivo
MainActivity.kt
referencie essa string:
Greeting(
name = getString(R.string.test), // ⬅️ Altere aqui
modifier = Modifier.padding(innerPadding)
)
-
É recomendado que todo texto utilizado em um aplicativo esteja registrado apenas em arquivos de recursos;
-
Agora clique sobre a pasta
app/src/main/res
e escolha a opção New > Android Resource File: -
Adicione o nome
strings.xml
e selecione opção Locale e clique no botão >>; -
Selecione português e clique em OK;
-
Agora adicione o seguinte conteúdo ao arquivo criado:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Olá Mundo TADS</string>
<string name="test">Isso é um Teste</string>
</resources>
-
Compile e execute o aplicativo;
-
Altere a configuração de Idioma do seu aparelho e veja que a aplicação se adapta de maneira automática.
DPI, Orientação e DIP
-
O Android possui uma especificação própria no que diz respeito ao tamanho e qualidade de tela de um dispositivo;
-
Um dos itens para definir a qualidade de uma tela é a unidade Dots per Inch (Pontos por polegadas) ou DPI:
-
O Android classifica os dispositivos em categorias de acordo com a quantidade de DPI existente. São elas:
-
-
Dessa forma, é possível adicionar imagens de diferentes tamanhos (em diferentes pastas de recursos) para que sejam selecionadas as melhores adaptadas à qualidade de tela do dispositivo;
-
É possível, também, definir recursos de acordo com a orientação atual da tela do dispositivo (horizontal ou vertical):
-
A tabela a seguir apresenta exemplos de recursos utilizados em diferentes orientações de tela:
-
-
Para determinarmos se uma tela é pequena ou grande, temos a seguinte lista de categorias:
-
Perceba que a unidade agora é DP (ou DIP - *Density Independent Pixels):
-
Essa é uma unidade que leva em consideração a quantidade de pixels na tela em sua área física;
-
Ou seja, quanto mais pixels tivermos por área, melhor será a qualidade da tela;
-
O DIP de uma tela é calculado da seguinte maneira:
dp = pixels / (dpi / 160)
-
Por exemplo, qual o tamanho da tela (para o Android) de um aparelho com resolução de 1024x768 pixels com densidade 240dpi (HDPI)?
-
Logo a tela irá se enquadrar na categoria large, pois tem 680x512dp;
-
A unidade DP é mais precisa para medir a qualidade das telas;
-
Para saber mais informações sobre seu dispositivo, adicione o seguinte código no método
onCreate(Bundle)
da MainActivity:val config = resources.configuration val metrics = resources.displayMetrics val orientation = config.orientation val density = metrics.density val height = metrics.heightPixels val width = metrics.widthPixels val mcc = config.mcc val mnc = config.mnc val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) config.locales[0] else config.locale val tag = "DS151" Log.d(tag, "density: $density") Log.d(tag, "orientation: $orientation") Log.d(tag, "height: $height") Log.d(tag, "width: $width") Log.d(tag, "language: ${locale.language} - ${locale.country}") Log.d(tag, "mcc: $mcc") Log.d(tag, "mnc: $mnc")
-
Para compilar o código sem erros, será necessário adicionar a dependência
android.util.Log
ao código:import android.util.Log
-
Se preferir, pressione o atalho ALT+Enter para que o Android Studio sugira a importação:
- Ou ainda, nas configurações do Android Studio, habilite a auto importação;
-
Agora, após executar a aplicação, consulte o menu View > Tool Windows > Logcat para verificar a saída do Log.
-
Jetpack Compose
-
Nova forma de produzir layouts de aplicações no Android Nativo;
- Substitui os antigos arquivos XML de views;
- É a forma padrão e indicada para a criação de novos layouts;
-
Traz muitas das ideias dos frameworks React Native e Flutter para o Android Nativo;
- O que torna a ideia muito interessante, unindo o melhor dos dois mundos;
-
Sistema de componentes de interface;
- Cada componente da interface será uma função do tipo Composable;
- A intenção é que o desenvolvedor crie uma coleção de funções, ou seja, de componentes, para que sejam reutilizadas na aplicação ou, inclusive, em outros projetos;
Funções Composable
-
São funções Kotlin que possuem o modificador
@Composable
:- O modificador permite que a função tenha acesso a outras funções do pacote Composable.
-
Tudo continua igual:
- O arquivo
AndroidManifest.xml
indica qual Activity é o Entry Point da aplicação; - Essa Activity determina o seu layout a partir da função
setContent
:- A diferença é que, ao invés de utilizar um arquivo XML de View, agora a função
setContent
recebe chamadas de funções Composable;
- A diferença é que, ao invés de utilizar um arquivo XML de View, agora a função
- O arquivo
-
Adicionar
Surface
em volta doText
e mudar a cor comSurface(color = MaterialTheme.colorScheme.primary)
:- A cor do texto muda automaticamente pois o material Design tem definições padrão para cores.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface( color = MaterialTheme.colorScheme.secondary ) {
Text(
text = "Hello Alex!",
modifier = modifier
)
}
}
Modifiers
- Composables geralmente possuem o parâmetro
modifier
para gerar alterações no seu layout:- Geralmente definem formas como o componente deve ser comportar em relação ao seu componente antecessor na hierarquia;
- Padding, Margin, Display, etc.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
-
É sempre interessante que um componente receba como parâmetro um objeto
Modifier
e passe esse parâmetro como argumento para o componente filho. -
Modifier
weight
ajuda a posicionar elementos. Algo como no Flex do CSS.
Layouts Básicos
Columns
,Row
,Box
;
Botões
Button
: último argumento é um trailing lamba, ou seja um bloco de código:- Trailing lambdas, quando são o último argumento, podem ser passados para fora dos parênteses dos argumentos da função;
- O que estiver dentro desse bloco será o conteúdo do botão.
- Precisa adicionar o argumento
onClick = {}
;
ElevatedButton(
onClick = {
Log.d("DS151", "clique!")
}
) {
Text("Clique")
}
Estado de componentes
-
Muito similar ao estado do React Native;
-
Algumas variáveis do componente podem ser monitoradas pelo Composable:
- Quando alteradas, o componente é redesenhado (a função é executada novamente): Recomposition
- Componentes podem ser reexecutados em qualquer ordem e frequentemente;
-
Para que o
Composable
observe uma variável utilizemutableStateOf
;- Além disso, para que a variável não perca o seu valor entre execuções, adicione
remember
:
- Além disso, para que a variável não perca o seu valor entre execuções, adicione
val expanded = remember { mutableStateOf(false) }
- Utilize, nesse exemplo,
expanded.value
para acessar ou alterar o valor do estado;
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded: MutableState<Boolean> = remember { mutableStateOf(false) }
Surface( color = MaterialTheme.colorScheme.secondary, modifier = modifier.padding(4.dp) ) {
Row(modifier = modifier.padding(24.dp)){
Column(modifier = modifier.padding(4.dp).weight(0.8F)){
Text(
text = "Hello",
)
Text(
text = "${name}!",
)
}
ElevatedButton(
modifier = modifier.weight(1.0F),
onClick = {
expanded.value = !expanded.value
}
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
Debug no Android Studio
-
Para realizar o debug da aplicação, apenas marque as linhas em que deseja adicionar um breakpoint:
-
Na sequência, basta clicar no botão Debug 'app';
-
Enquanto o breakpoint estiver ativo, é possível inspecionar as variáveis da aplicação na janela Debug;
Códigos da aula
Os códigos utilizados nessa aula podem ser encontrados no seguinte repositório:
https://github.com/tads-ufpr-alexkutzke/ds151-aula-02-codes
Referências
- Google, Developer Guides https://developer.android.com/guide acessado em 12/03/2025.
- Glauber, Nelson, Dominando o Android com Kotlin, São Paulo : Novatec, 2019, 3ed.
Antigo (para referência)
findViewByID
e evento de clique
-
Assim como na programação Web, é necessário que exista algum tipo de comunicação entre o layout (tela e componentes) e o código de execução da aplicação (Activities);
-
Para tanto, faremos um exemplos de interação entre layout e a MainActivity:
-
No arquivo
res/layout/activity_main.xml
, remova o TextView que contém o Hello World e, em seguida, adicione um Plain Text; -
Abaixo dele, adicione um Button:
- Não se preocupe com o alinhamento nesse momento;
- Apenas clique no botão Infer constraints;
-
Perceba que, ao lado do layout estão as propriedades do objeto selecionado;
-
Existem duas propriedades nomeadas com text. Nos preocuparemos apenas com a segunda (sem um ícone);
-
Selecione o botão ... nessa propriedade text;
-
A tela que surgir é utilizada para criar recursos de texto (strings). Selecione o botão Add new resource e, então, New string value;;
-
Preencha com os valores
main_button_toast
como nome eExibir toast
como valor; -
Essa mesma alteração poderia ter sido realizada diretamente no arquivo
res/values/strings.xml
. Nas próximas vezes, proceda como preferir; -
No arquivo do layout, altere a propriedade ID do botão para
buttonToast
; -
Altere o ID do Plain Text para
editText
e apague sua propriedade text. O layout ficará semelhante a esse:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:text="" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/buttonToast" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="47dp" android:text="@string/main_button_toast" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/editText" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
Agora adicione o seguinte código a sua Main Activity:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val editText = findViewById<EditText>(R.id.editText) val button = findViewById<Button>(R.id.buttonToast) button.setOnClickListener { val text = editText.text.toString() Toast.makeText(this,text,Toast.LENGTH_SHORT).show() } } }
-
-
O método
setOnClickListener(View.OnClickListener)
precisa de um objeto que implemente a interfaceView.OnClickListener
para registrar um novo evento de click para o botão;; -
Para interfaces que possuem apenas um método, o caso da
View.OnClickListener
, podemos utilizar uma expressão lambda. É exatamente o que foi feito no código acima; -
A versão completa da declaração seria algo como:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val editText = findViewById<EditText>(R.id.editText) val button = findViewById<Button>(R.id.buttonToast) button.setOnClickListener (object: View.OnClickListener{ override fun onClick(v: View?) { val text = editText.text.toString() Toast.makeText(this@MainActivity,text,Toast.LENGTH_SHORT).show() } }) } }
Kotlin Android Extensions
- O método
findViewById(int)
é padrão do Android e bastante utilizado para acessar elementos do layout; - Entretanto, com o plugin Kotlin Android Extensions, dentre outras coisas, permite que o acesso aos elementos do layout seja mais direto;
- Para testar, verifique se a seguinte linha está presente no arquivo
build.gradle
no móduloapp
:
apply plugin: 'kotlin-android-extensions'
- Agora, na Main Activity, importe seu layout da seguinte maneira:
import kotlinx.android.synthetic.main.activity_main.*
- Dessa forma, é possível adicionar os componentes do layout diretamente:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//val editText = findViewById<EditText>(R.id.editText)
//val button = findViewById<Button>(R.id.buttonToast)
buttonToast.setOnClickListener (object: View.OnClickListener{
override fun onClick(v: View?) {
val text = editText.text.toString()
Toast.makeText(this@MainActivity,text,Toast.LENGTH_SHORT).show()
}
})
}
}
O tema da interface é o Catppuchin Latte.
Razão para eu tentar desenvolvimento nativo novamente. Passei um tempo com React Native.
Aula 03: Introdução ao Kotlin para Desenvolvimento Android
Introdução ao Kotlin
Kotlin é uma linguagem moderna e concisa, totalmente interoperável com Java, e é a linguagem oficial para desenvolvimento Android.
Vantagens do Kotlin:
- Sintaxe concisa e expressiva;
- Segurança contra NullPointerExceptions;
- Suporte a programação funcional;
- Interoperabilidade com Java;
- Coroutines para programação assíncrona;
Exemplo 1: Olá, Mundo!
fun main() {
println("Olá, Mundo!")
}
A função main
é o ponto de entrada do programa Kotlin. A palavra-chave fun
indica a definição de uma função, main
é o nome da função, e os parênteses ()
indicam que ela não recebe argumentos. O corpo da função é delimitado por {}
.
Variáveis e Tipos de Dados
Em Kotlin, variáveis podem ser declaradas de duas formas principais:
val
(imutável, equivalente afinal
em Java)var
(mutável, pode ter seu valor alterado posteriormente)
Exemplo 2: Declarando variáveis
val nome: String = "João"
var idade: Int = 25
idade = 26 // Permitido pois "idade" é mutável
A sintaxe segue o formato val/var nome: Tipo = valor
. O tipo pode ser inferido automaticamente pelo compilador.
Controle de Fluxo
O controle de fluxo em Kotlin é essencial para definir a lógica de execução dos programas. Ele permite que o código tome decisões e repita ações com base em condições específicas. As principais estruturas de controle de fluxo em Kotlin incluem:
Estruturas Condicionais
if / else
Sintaxe:
if (condicao) {
// Código executado se a condição for verdadeira
} else {
// Código executado se a condição for falsa
}
Exemplo:
val numero = 10
if (numero > 0) {
println("O número é positivo")
} else {
println("O número é negativo ou zero")
}
O if
também pode ser usado como expressão retornando um valor:
val resultado = if (numero % 2 == 0) "Par" else "Ímpar"
println(resultado)
when
O when
substitui o switch
de outras linguagens e permite comparar um valor com múltiplas condições.
Sintaxe:
when (variavel) {
valor1 -> { // Código }
valor2 -> { // Código }
else -> { // Código }
}
Exemplo:
val dia = 3
val nomeDoDia = when (dia) {
1 -> "Domingo"
2 -> "Segunda-feira"
3 -> "Terça-feira"
4 -> "Quarta-feira"
5 -> "Quinta-feira"
6 -> "Sexta-feira"
7 -> "Sábado"
else -> "Dia inválido"
}
println(nomeDoDia)
O when
também pode ser usado com expressões mais complexas:
val idade = 25
when {
idade < 12 -> println("Criança")
idade in 12..17 -> println("Adolescente")
else -> println("Adulto")
}
Estruturas de Repetição
while
Sintaxe:
while (condicao) {
// Código executado repetidamente
}
Exemplo:
var contador = 1
while (contador <= 5) {
println("Contador: $contador")
contador++
}
do-while
O do-while
executa pelo menos uma vez, pois a verificação da condição ocorre após a execução do bloco.
Sintaxe:
do {
// Código executado ao menos uma vez
} while (condicao)
Exemplo:
var numero = 0
do {
println("Número: $numero")
numero++
} while (numero < 3)
for
O for
é usado para iterar sobre intervalos, listas e outras coleções.
Sintaxe:
for (item in colecao) {
// Código executado para cada item
}
Exemplo com intervalos:
for (i in 1..5) {
println("Número: $i")
}
Exemplo com listas:
val frutas = listOf("Maçã", "Banana", "Laranja")
for (fruta in frutas) {
println(fruta)
}
Exemplo com índice:
val nomes = listOf("Ana", "Bruno", "Carlos")
for ((indice, nome) in nomes.withIndex()) {
println("$indice: $nome")
}
Controle de Loop: break e continue
break
: interrompe o loop completamente.continue
: pula para a próxima iteração do loop.
Exemplo:
for (i in 1..10) {
if (i == 5) break
println(i)
}
Saída:
1
2
3
4
for (i in 1..10) {
if (i % 2 == 0) continue
println(i)
}
Saída:
1
3
5
7
9
Funções em Kotlin
As funções são blocos de código reutilizáveis que executam uma tarefa específica. No Kotlin, as funções podem ter parâmetros, retornar valores e até serem funções de alta ordem.
Declaração de Funções
A palavra-chave fun
é usada para declarar uma função em Kotlin.
Sintaxe básica:
fun nomeDaFuncao(parametros): TipoDeRetorno {
// Corpo da função
return valor
}
Exemplo:
fun soma(a: Int, b: Int): Int {
return a + b
}
val resultado = soma(3, 5)
println(resultado) // Saída: 8
Funções com Retorno Unit
Se uma função não retorna um valor, seu tipo de retorno é Unit
(equivalente a void
em outras linguagens). O Unit
pode ser omitido.
fun imprimirMensagem(mensagem: String): Unit {
println(mensagem)
}
imprimirMensagem("Olá, Kotlin!")
Funções de Uma Linha (Expressões)
Se a função consiste em apenas uma expressão, podemos usar a sintaxe simplificada:
fun multiplicar(a: Int, b: Int) = a * b
println(multiplicar(4, 3)) // Saída: 12
Parâmetros com Valores Padrão
Podemos definir valores padrão para parâmetros:
fun saudar(nome: String = "Visitante") {
println("Olá, $nome!")
}
saudar() // Saída: Olá, Visitante!
saudar("Carlos") // Saída: Olá, Carlos!
Parâmetros Nomeados
Podemos chamar funções especificando os nomes dos parâmetros para maior clareza:
fun formatarTexto(texto: String, repetir: Int = 1, maiusculo: Boolean = false) {
val resultado = if (maiusculo) texto.uppercase() else texto
repeat(repetir) { println(resultado) }
}
formatarTexto(repetir = 3, texto = "Oi", maiusculo = true)
Funções de Extensão
No Kotlin, podemos adicionar novas funções a classes existentes sem modificá-las, usando funções de extensão.
fun String.reverter(): String {
return this.reversed()
}
println("Kotlin".reverter()) // Saída: niltoK
Funções Lambda (Funções Anônimas)
Kotlin permite definir funções anônimas (lambdas) que podem ser atribuídas a variáveis ou passadas como argumentos.
val soma = { a: Int, b: Int -> a + b }
println(soma(5, 7)) // Saída: 12
Funções de Alta Ordem
Funções que recebem outras funções como parâmetro ou retornam funções são chamadas de alta ordem.
fun operacao(a: Int, b: Int, funcao: (Int, Int) -> Int): Int {
return funcao(a, b)
}
val resultadoSoma = operacao(10, 20, ::soma)
println(resultadoSoma) // Saída: 30
No exemplo acima, usamos ::soma
para referenciar diretamente a função soma
. O operador ::
é usado para obter uma referência de função, permitindo que a função seja passada como argumento sem ser executada imediatamente.
Trailing Lambdas
Quando o último (ou único) parâmetro de uma função é uma função lambda, podemos movê-la para fora dos parênteses, tornando o código mais legível. Esse recurso é chamado de trailing lambda.
Exemplo sem trailing lambda:
fun executar(acao: () -> Unit) {
acao()
}
executar({ println("Executando ação!") })
Exemplo com trailing lambda:
executar {
println("Executando ação!")
}
Esse estilo é amplamente utilizado em bibliotecas Kotlin, como nas funções forEach
de listas:
val numeros = listOf(1, 2, 3, 4)
numeros.forEach {
println(it)
}
Coleções em Kotlin
As coleções em Kotlin são estruturas de dados que armazenam múltiplos elementos. Existem três tipos principais de coleções:
- List: Uma coleção ordenada de elementos.
- Set: Uma coleção de elementos únicos.
- Map: Uma coleção de pares chave-valor.
Listas (List)
Uma List
é uma coleção ordenada de elementos, que pode ser mutável ou imutável.
Lista imutável (List)
val listaImutavel = listOf("Maçã", "Banana", "Laranja")
println(listaImutavel[0]) // Saída: Maçã
Lista mutável (MutableList)
val listaMutavel = mutableListOf("Maçã", "Banana")
listaMutavel.add("Laranja")
println(listaMutavel) // Saída: [Maçã, Banana, Laranja]
Operações comuns com List
val numeros = listOf(1, 2, 3, 4, 5)
println(numeros.size) // Obtém o tamanho da lista
println(numeros.contains(3)) // Verifica se um elemento existe
println(numeros.first()) // Primeiro elemento
println(numeros.last()) // Último elemento
Conjuntos (Set)
Os conjuntos (Set
) armazenam elementos únicos e não garantem uma ordem específica.
Set imutável
val setImutavel = setOf(1, 2, 3, 3)
println(setImutavel) // Saída: [1, 2, 3]
Set mutável
val setMutavel = mutableSetOf(1, 2, 3)
setMutavel.add(4)
setMutavel.add(2) // Elemento duplicado não é adicionado
println(setMutavel) // Saída: [1, 2, 3, 4]
Mapas (Map)
Os mapas (Map
) armazenam pares chave-valor.
Map imutável
val mapaImutavel = mapOf("nome" to "Carlos", "idade" to 30)
println(mapaImutavel["nome"]) // Saída: Carlos
Map mutável
val mapaMutavel = mutableMapOf("nome" to "Ana")
mapaMutavel["idade"] = 25
println(mapaMutavel) // Saída: {nome=Ana, idade=25}
Operações com Map
val mapa = mapOf(1 to "um", 2 to "dois", 3 to "três")
println(mapa.keys) // Obtém as chaves
println(mapa.values) // Obtém os valores
println(mapa.containsKey(2)) // Verifica se a chave existe
Iteração sobre Coleções
Iterando com for
val lista = listOf("A", "B", "C")
for (item in lista) {
println(item)
}
Usando forEach
lista.forEach { println(it) }
Filtragem e Transformação
Filtragem (filter
)
val numeros = listOf(1, 2, 3, 4, 5)
val pares = numeros.filter { it % 2 == 0 }
println(pares) // Saída: [2, 4]
Transformação (map
)
val dobrado = numeros.map { it * 2 }
println(dobrado) // Saída: [2, 4, 6, 8, 10]
Orientação a Objetos em Kotlin
A Orientação a Objetos (OO) é um paradigma de programação que organiza o código em torno de objetos. No Kotlin, podemos trabalhar com classes, herança, encapsulamento, polimorfismo e interfaces de forma simples e intuitiva.
Classes e Objetos
Uma classe é um modelo para criar objetos. Em Kotlin, usamos a palavra-chave class
para definir classes.
Exemplo de classe e objeto:
class Pessoa(val nome: String, var idade: Int) {
fun saudacao() {
println("Olá, meu nome é $nome e tenho $idade anos.")
}
}
val pessoa = Pessoa("Carlos", 30)
pessoa.saudacao() // Saída: Olá, meu nome é Carlos e tenho 30 anos.
Construtores
Podemos definir construtores primários diretamente na declaração da classe e construtores secundários dentro do corpo da classe.
Construtor primário:
class Carro(val marca: String, val modelo: String)
val carro = Carro("Toyota", "Corolla")
println(carro.marca) // Saída: Toyota
Construtor secundário:
class Animal {
var nome: String
var especie: String
constructor(nome: String, especie: String) {
this.nome = nome
this.especie = especie
}
}
val cachorro = Animal("Rex", "Cachorro")
println(cachorro.nome) // Saída: Rex
Herança
Kotlin permite herança entre classes usando open
para permitir que uma classe seja estendida.
open class Animal(val nome: String) {
open fun fazerSom() {
println("Som genérico")
}
}
class Cachorro(nome: String) : Animal(nome) {
override fun fazerSom() {
println("Au Au")
}
}
val cachorro = Cachorro("Bolt")
cachorro.fazerSom() // Saída: Au Au
Modificadores de Visibilidade
Kotlin oferece quatro modificadores de visibilidade:
public
(padrão) – acessível de qualquer lugar.private
– acessível apenas dentro da classe.protected
– acessível dentro da classe e subclasses.internal
– acessível dentro do mesmo módulo.
class ContaBancaria(private val saldo: Double) {
fun exibirSaldo() {
println("Saldo: $saldo")
}
}
val conta = ContaBancaria(1000.0)
conta.exibirSaldo() // Saída: Saldo: 1000.0
Classes Abstratas
Classes abstratas não podem ser instanciadas diretamente e servem como modelo para subclasses.
abstract class SerVivo(val nome: String) {
abstract fun mover()
}
class Peixe(nome: String) : SerVivo(nome) {
override fun mover() {
println("O peixe está nadando")
}
}
val peixe = Peixe("Nemo")
peixe.mover() // Saída: O peixe está nadando
Interfaces
Interfaces definem comportamentos que podem ser implementados por várias classes.
interface Nadador {
fun nadar() {
println("Estou nadando!")
}
}
class Golfinho : Nadador
val golfinho = Golfinho()
golfinho.nadar() // Saída: Estou nadando!
Data Classes
As data class
são usadas para armazenar dados de forma eficiente, gerando automaticamente métodos como toString()
, equals()
, e copy()
.
data class Usuario(val nome: String, val idade: Int)
val usuario1 = Usuario("Ana", 25)
val usuario2 = usuario1.copy(idade = 30)
println(usuario1) // Saída: Usuario(nome=Ana, idade=25)
println(usuario2) // Saída: Usuario(nome=Ana, idade=30)
Exercícios Práticos para entregar
https://gitlab.com/ds151-alexkutzke/ds151-kotlin-assignment
Referências
- Documentação oficial do Kotlin
- Guia para desenvolvimento Android com Kotlin
- Kotlinlang - Introdução à Linguagem
- JetBrains Kotlin Playground
- Repositório oficial do Kotlin no GitHub
Aula 04: Introdução ao Jetpack Compose
Código da aula
https://github.com/tads-ufpr-alexkutzke/ds151-aula-04-codes-jetpack-compose/
Referências
- Pensando com Jetpack Compose
- Passo-a-passo de exemplo sobre Compose
- Sobre o pacote
androidx.compose
- Jetpack Compose Playground
Jetpack Compose: A Nova Era do Desenvolvimento de Interfaces Android
- Toolkit Moderno do Android: Usado para construir interfaces de usuário (UI) de forma declarativa.
- Diferença do Sistema Tradicional:
- XML Tradicional: Baseado em XML.
- Compose: Descreve a UI como uma função transformando dados em elementos visuais.
Benefícios do Jetpack Compose
- Código Mais Simples e Conciso:
- Reduz significativamente a quantidade de código necessário para criar interfaces.
- Reutilização de Componentes:
- Facilita a criação de componentes que são reutilizáveis e personalizados.
- Atualizações Dinâmicas:
- Apenas as partes da UI que precisam são recompostas, melhorando a performance.
- Compatibilidade com Kotlin:
- Integra-se perfeitamente com a linguagem Kotlin, aproveitando ao máximo seus recursos modernos.
- Animações Simplificadas:
- Possui APIs poderosas para a criação de animações complexas com facilidade.
Conceitos Fundamentais
Funções de Composição
No Jetpack Compose, a UI é construída com funções chamadas funções de composição. Essas funções são anotadas com @Composable e descrevem um pedaço da UI.
@Composable
fun Greeting(name: String) {
Text(text = "Olá, $name!")
}
Estado (State)
O estado é qualquer dado que pode mudar e influenciar a UI. Em Compose, usamos remember e mutableStateOf para armazenar e observar o estado.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text(text = "Contador: $count")
}
}
Modificadores (Modifiers)
Modificadores são usados para decorar ou adicionar comportamento aos elementos da UI. Eles são encadeáveis e permitem configurar propriedades como tamanho, padding, cor, etc.
Text(
text = "Olá, Compose!",
modifier = Modifier
.padding(16.dp)
.background(Color.LightGray)
)
Layouts
Compose oferece vários layouts para organizar elementos na tela. Alguns dos mais comuns incluem:
- Column: Organiza os elementos verticalmente.
- Row: Organiza os elementos horizontalmente.
- Box: Permite sobrepor elementos.
@Composable
fun MyLayout() {
Column {
Text(text = "Elemento 1")
Text(text = "Elemento 2")
}
}
Exemplos Práticos
Exemplo 1: Lista Simples
Criar uma lista de itens usando LazyColumn.
@Composable
fun SimpleList() {
val items = listOf("Item 1", "Item 2", "Item 3")
LazyColumn {
items(items) { item ->
Text(text = item, modifier = Modifier.padding(8.dp))
}
}
}
Exemplo 2: Formulário de Login
Implementar um formulário de login com campos de texto e um botão.
@Composable
fun LoginForm() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = username,
onValueChange = { username = it },
label = { Text("Usuário") }
)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("Senha") },
visualTransformation = PasswordVisualTransformation()
)
Button(onClick = { /* Lógica de login aqui */ }) {
Text("Entrar")
}
}
}
Pensando com Jetpack Compose
1. Paradigma de Programação Declarativa
- Modelo Tradicional: Uso de uma hierarquia de visualização em árvore.
- Desafios: Alterações manuais que podem levar a erros e estados ilegais.
- Abordagem Declarativa: Simplifica a engenharia de UI ao aplicar mudanças necessárias, evitando complexidade de atualizações manuais.
2. Funções Composable
- Funções anotadas com
@Composable
para converter dados em UI. - Funções que não retornam nada, emitindo diretamente o estado desejado da tela.
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name")
}
3. Recomposição
-
Modelo de UI Imperativa vs Compose
- No modelo imperativo, altere o estado de um widget usando setters.
- No Compose, atualize chamando novamente a função Composable com novos dados.
- Widgets são redesenhados apenas se necessário, graças à recomposição inteligente do Compose.
-
Exemplo de Função Composable
-
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
- Ao clicar no botão, a chamada atualiza
clicks
e a funçãoText
é recomposta para mostrar o novo valor.
-
-
Eficiência na Recomputação
- Recomputação inteligente do Compose apenas para componentes que mudaram.
- Isso economiza recursos computacionais e preserva a duração da bateria.
- Ignora funções/lambdas sem parâmetros alterados, evitando recalcular desnecessariamente.
-
Cuidados com Efeitos Colaterais
- Evite depender de efeitos colaterais em funções composable.
- Efeitos colaterais perigosos incluem:
- Alterar propriedades de objetos compartilhados.
- Atualizar elementos observáveis em ViewModel.
- Modificar preferências compartilhadas.
- Recomenda-se executar operações de alto custo em corrotinas em segundo plano.
-
Exemplo: Uso de SharedPreferences em Composable
-
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
- Atualizações de valor são geridas via callbacks e operações de fundo no ViewModel.
-
-
Considerações para Uso Eficiente do Compose
- Recomposição Optimizada:
- A recomposição ignora funções/lambdas quando possível.
- Pode ser cancelada se for desnecessária.
- Execução e Performance:
- Funções composable podem ser executadas em todos os frames de uma animação.
- São executadas em paralelo e podem ocorrer em qualquer ordem.
- Recomposição Optimizada:
-
Melhores Práticas
- Funções composable devem ser rápidas, idempotentes e sem efeitos colaterais para garantir compatibilidade e eficiência na recomposição.
Recomposição no Compose: Otimizações
-
Recomposição Seletiva
- O Compose limita a recomposição às partes da IU que realmente precisam de atualização.
- Permite recompor um único elemento (por exemplo, um botão) sem afetar outros elementos na árvore da IU.
-
Funções e Lambdas Composable
-
Todas as funções e lambdas podem ser recompostas individualmente.
-
Exemplo de como a recomposição pode ignorar elementos não alterados ao renderizar uma lista:
/** * Exibe uma lista de nomes que o usuário pode clicar com um cabeçalho */ @Composable fun NamePicker( header: String, names: List<String>, onNameClicked: (String) -> Unit ) { Column { // Recompõe quando [header] muda, mas não quando [names] muda Text(header, style = MaterialTheme.typography.bodyLarge) HorizontalDivider() // LazyColumn é a versão do Compose para RecyclerView. // A lambda passada para items() é semelhante a um RecyclerView.ViewHolder. LazyColumn { items(names) { name -> // Recompõe quando o [name] do item atualiza. Não recompõe quando [header] muda NamePickerItem(name, onNameClicked) } } } } /** * Exibe um nome individual que o usuário pode clicar. */ @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable(onClick = { onClicked(name) })) }
-
-
Escopos de Recomputação
- Elementos podem ser recompostos individualmente sem necessidade de executar pais ou irmãos.
- Alteração no
header
permite pular para a execução daColumn
, enquanto ignoraLazyColumn
senames
não mudou.
-
Cuidados com Efeitos Colaterais
- Execução de funções composable não deve ter efeitos colaterais.
- Efeitos colaterais devem ser acionados a partir de callbacks, garantindo que a recomposição não altere o estado de forma indesejada.
4. Recomposição Otimista
- Permite cancelamento e reinício da recomposição com novos parâmetros.
- Necessário garantir funções idempotentes e sem efeitos colaterais para manter a integridade.
5. Recomendações para Desenvolvimento
- Utilizar funções rápidas e sem efeitos colaterais.
- Evitar operações caras durante recomposição.
- Mover trabalhos intensivos para outras threads e utilizar estados mutáveis ou LiveData para gerenciar dados.
Exemplo sobre execução em paralelo
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
Explicação:
- Layout Estruturado:
Row
comArrangement.SpaceBetween
: Os elementos ficam distribuídos ao longo do espaço disponível.Column
: Lista os itens fornecidos.
- Contagem de Itens: Mostra a contagem total de itens após a coluna.
@Composable
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Card {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
}
Text("Count: $items")
}
}
Ordem de execução
- Execução pode ocorrer em qualquer ordem;
- Por exemplo, no código abaixo:
- Se
StartScreen
seta uma variável global (um efeito colateral) e que é utilizada por, digamos,MiddleScreen
, não há garantias se a variável estará setada quandoMiddleScreen
rodar;
- Se
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
Tutorial Passo-a-passo
Resumo e tradução do tutorial da documentação oficial.
Conceitos Básicos de Compose
-
Funções de Composição (Composable Functions)
- Define a UI do app programaticamente.
- Use a anotação
@Composable
para funções de composição. - Exemplo: Adicionar um elemento de texto com a função
Text
.
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
-
Visualização de Funções de Composição
- Utilizar a anotação
@Preview
para visualizar funções no Android Studio. - Criação de uma função de pré-visualização para ver a composição sem parâmetros.
import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
- Utilizar a anotação
Desenvolvimento de Layouts
-
Hierarquia de Elementos de UI
- Construir hierarquias de UI com funções de composição.
- Exemplo: Usar
Column
para dispor elementos verticalmente.
import androidx.compose.foundation.layout.Column data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) }
-
Adicionar Elementos de Texto e Imagem
- Exemplo de código para adicionar múltiplos textos e imagens.
- Uso de
Row
eImage
para estruturar elementos.
import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.ui.res.painterResource @Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }
- Baixe a imagem de exemplo:
- Utilize o Resource Manager no Android Studio para adicionar a imagem aos recursos da aplicação.
-
Configuração do Layout
- Uso de modificadores para alterar tamanho, layout e aparência.
- Exemplo de código com modificadores como
padding
,size
eclip
.
import androidx.compose.foundation.border import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier .size(40.dp) .clip(CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text(text = msg.author) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
Design com Material Design
-
Uso do Design Material
- Implementação de Material Design 3.
- Uso de
Surface
e temaComposeTutorialTheme
para consistência de estilo.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { Aula04JetPackComposeTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MessageCard(Message("Android", "Jetpack Compose")) } } } } } @Preview @Composable fun PreviewMessageCard() { Aula04JetPackComposeTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
-
Aplicando Estilos com
MaterialTheme
- Cores: Uso de
MaterialTheme.colorScheme
. - Tipografia: Uso de
MaterialTheme.typography
. - Formas: Ajustes de bordas e elevação de elementos.
import androidx.compose.foundation.border import androidx.compose.material3.MaterialTheme @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.bodyMedium ) } } }
- Cores: Uso de
Trabalhando com Temas
-
Habilitação do Tema Escuro
- Suporte nativo a tema escuro com adaptação de cores automáticas.
- Uso de múltiplas anotações de pré-visualização para ver temas claro e escuro.
import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { Aula04JetPackComposeTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
Listas e Animações
-
Criação de Listas de Mensagens
- Uso de
LazyColumn
eLazyRow
para eficiência. - Exemplo de código para criar uma função
Conversation
exibindo múltiplas mensagens.
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { Aula04JetPackComposeTheme { Conversation(SampleData.conversationSample) } }
Adicione as imagens fictícias no arquivo
MainActivity.kt
/** * SampleData for Jetpack Compose Tutorial */ object SampleData { // Sample conversation data val conversationSample = listOf( Message( "Lexi", "Test...Test...Test..." ), Message( "Lexi", """List of Android versions: |Android KitKat (API 19) |Android Lollipop (API 21) |Android Marshmallow (API 23) |Android Nougat (API 24) |Android Oreo (API 26) |Android Pie (API 28) |Android 10 (API 29) |Android 11 (API 30) |Android 12 (API 31)""".trim() ), Message( "Lexi", """I think Kotlin is my favorite programming language. |It's so much fun!""".trim() ), Message( "Lexi", "Searching for alternatives to XML layouts..." ), Message( "Lexi", """Hey, take a look at Jetpack Compose, it's great! |It's the Android's modern toolkit for building native UI. |It simplifies and accelerates UI development on Android. |Less code, powerful tools, and intuitive Kotlin APIs :)""".trim() ), Message( "Lexi", "It's available from API 21+ :)" ), Message( "Lexi", "Writing Kotlin for UI seems so natural, Compose where have you been all my life?" ), Message( "Lexi", "Android Studio next version's name is Arctic Fox" ), Message( "Lexi", "Android Studio Arctic Fox tooling for Compose is top notch ^_^" ), Message( "Lexi", "I didn't know you can now run the emulator directly from Android Studio" ), Message( "Lexi", "Compose Previews are great to check quickly how a composable layout looks like" ), Message( "Lexi", "Previews are also interactive after enabling the experimental setting" ), Message( "Lexi", "Have you tried writing build.gradle with KTS?" ), ) }
- Uso de
-
Animação de Mensagens
- Uso de funções
remember
emutableStateOf
para gerenciar estado local. - Exemplo de código para animar cor e tamanho de mensagens ao expandir.
import androidx.compose.foundation.clickable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) var isExpanded by remember { mutableStateOf(false) } val surfaceColor by animateColorAsState( if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, ) Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp, color = surfaceColor, modifier = Modifier.animateContentSize().padding(1.dp) ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.bodyMedium ) } } } }
- Uso de funções
Exercícios para praticar
Exercício 1: Tela simples centralizada
Altere o código do repositório da aula de modo que o componente SimpleScreen
fique com a seguinte aparência:
Para esse exercício, os seguintes recursos do Jetpack Compose podem ser interessantes:
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.Alignment
Exercício 2: Criar um Perfil de Usuário
Crie uma tela que exiba informações de um perfil de usuário, como nome, foto e descrição. Utilize Column, Row, Image e Text para estruturar a UI.
Aula 05: Layouts básicos com Jetpack Compose
Fundamentos de Layouts
Componentes Principais
Os componentes básicos de layout no Compose são Row
, Column
, e Box
. Esses componentes permitem arranjar elementos na vertical, horizontal, ou de forma sobreposta.
A Column é um componente que alinha seus filhos verticalmente. Cada elemento filho é empilhado abaixo do anterior. É útil para criar layouts estruturados verticalmente, como listas ou seções de informações. Você pode ajustar o espaçamento entre os elementos e o alinhamento dentro da coluna para personalizar a aparência de acordo com as necessidades do seu design.
A Row é semelhante à Column, mas alinha seus filhos horizontalmente. Os elementos são posicionados lado a lado, permitindo a criação de layouts horizontais como fileiras de botões ou ícones. Assim como com Columns, você pode especificar o espaçamento e alinhamento dos elementos para controlar exatamente como eles são apresentados na tela.
O Box é um componente flexível que permite sobrepor elementos um sobre o outro. Ele funciona como um contêiner de layout onde você pode posicionar elementos em camadas. Isso é útil para criar designs complexos ou personalizados onde os elementos se sobrepõem parcialmente ou ficam centralizados dentro de uma área especificada.
@Composable
fun RowColumnExample() {
Column {
Text("Column Item 1")
Text("Column Item 2")
Row {
Text("Row Item 1")
Text("Row Item 2")
}
}
}
Modificadores
Modifiers, ou modificadores, são ferramentas no Jetpack Compose que permitem modificar ou estilizar os componentes. Eles podem ser usados para alterar propriedades como tamanho, padding, background, e até funcionalidades de clique. Modifiers são aplicados de maneira encadeada, proporcionando uma maneira flexível e declarativa de personalizar a UI.
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
@Composable
fun StyledText() {
Text(
text = "Styled with Modifiers",
modifier = Modifier
.background(Color.Cyan)
.padding(16.dp)
.width(200.dp)
.height(50.dp)
)
}
Modificadores fillMaxWidth
e padding
.
Modificadores podem ter sobrecargas, permitindo, por exemplo, especificar diferentes maneiras de criar padding.
Arrangement e Alignment
O Alignment define como os elementos são alinhados dentro de um contêiner, como Row, Column, ou Box. Ele determina a posição dos elementos no eixo vertical ou horizontal (eixo principal de cada elemento), oferecendo controle preciso sobre como os componentes são posicionados tanto na direção principal quanto na direção cruzada do layout.
O Arrangement controla o espaçamento entre os elementos filhos dentro de Rows ou Columns, utilizando o eixo secundário como referência. Ele permite especificar como os elementos devem ser distribuídos, seja com espaçamento entre eles, tudo no início, centro ou fim do layout. Isso facilita a criação de interfaces esteticamente agradáveis ao controlar como o espaço extra é utilizado.
@Composable
fun AlignedContent() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Left")
Text("Right")
}
}
Box
O Box
pode ser utilizado para sobrepor itens e criar designs complexos onde os elementos precisam estar um em cima do outro.
@Composable
fun BoxExample() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.LightGray)
) {
Text("Bottom", Modifier.align(Alignment.BottomStart))
Text("Top", Modifier.align(Alignment.TopEnd))
}
}
Lazy Layouts
Utilize LazyColumn
e LazyRow
para listas eficientes e pagináveis, que são essenciais para tratar grandes quantidades de dados sem comprometer a performance.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@Composable
fun SimpleLazyColumn() {
LazyColumn {
items(listOf("Apple", "Banana", "Cherry")) { fruit ->
Text("Fruit: $fruit", Modifier.padding(8.dp))
}
}
}
Customização e Temas
Themes
A customização de temas no Jetpack Compose permite adaptar a aparência do aplicativo em nível global. Utilizando o MaterialTheme, você pode definir cores, tipografia, e formas para fornecer uma identidade visual consistente em toda a aplicação. A personalização de temas oferece uma maneira eficiente de garantir uma experiência de usuário coerente e estilizada.
Abra o arquivo ui/theme/Theme.kt
, você verá detalhes da implementação do tema da aplicação.
MaterialTheme
é uma função composable que reflete os princípios de estilo da especificação do Material Design.
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
@Composable
fun ThemedGreeting() {
MaterialTheme(
colors = MaterialTheme.colors.copy(
primary = Color.Magenta,
primaryVariant = Color.DarkMagenta,
secondary = Color.Cyan
)
) {
Surface {
Text("Hello with Theme")
}
}
}
Para o uso de Ícones
Use o composable IconButton
, por exemplo:
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
Adicione a seguinte linha às dependências no seu arquivo app/build.gradle.kts
para mais ícones:
implementation("androidx.compose.material:material-icons-extended")
Práticas Avançadas
Criando Layouts Compostos
Layouts compostos personalizados que melhor se encaixam nas necessidades específicas do design do aplicativo.
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
@Composable
fun CustomButton(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.padding(8.dp)
.background(Color.Blue)
.clickable { }
.padding(16.dp)
) {
content()
}
}
@Composable
fun CustomButtonExample() {
CustomButton {
Text("Click Me!")
}
}
Interoperabilidade com Views Tradicionais
É possível utilizar Views
dentro do Compose e vice-versa, facilitando a migração de aplicativos existentes para o novo sistema.
// No arquivo MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidViewExample()
}
}
}
@Composable
fun AndroidViewExample() {
AndroidView(
factory = { context ->
Button(context).apply {
text = "Old View Button"
setOnClickListener {
// Código do botão ao ser clicado
}
}
}
)
}
note
- Diferença entre componentes
Surface
eScaffold
:- São estruturas fundamentais de layout. Algo como containers;
- Porém,
Scaffold
torna mais fácil o tratamento de área cobertas pelo Sistema Operacional (como barras de notificação, notch de câmera, botões de navegação, etc.); - Isso é ainda mais importante após a decisão do Google de que as aplicações devem usar
enableEdgeToEdge()
.- Isso significa que a aplicação é que deve tomar conta dessas áreas cobertas, ocupando a tela de "borda à borda";
- Com o
Surface
esse controle se torna bem mais complexo.
Referências
- Layouts em Jetpack Compose
- Modificadores e Layouts no Compose
- Theming em Jetpack Compose
- Jetpack Compose - Animações
- Interoperabilidade com Compose
Exemplo Prático -> RGB Counter
Link do Repositório
https://github.com/tads-ufpr-alexkutzke/ds151-aula-05-counter
Desenvolvimento passo a passo (commits)
Cria Counter Composable
Estrutura básica para o contador
O modificador weight
faz o elemento preencher todo o espaço disponível, afastando os outros elementos que não têm peso, chamados de inflexíveis.
Tentativa frustada de incrementar o contador
O motivo pelo qual a mutação dessa variável não aciona recomposições é que ela não está sendo rastreada pelo Compose. Além disso, cada vez que Greeting
é chamado, a variável será redefinida para false.
Use o mutableStateOf
.
No entanto, você não pode simplesmente atribuir mutableStateOf
a uma variável dentro de um composable.
Para preservar o estado entre recomposições, lembre o estado mutável usando remember
.
Primeiro contador funcional
Se o estado mudar, os composables que leem esses campos serão recompostos para exibir as atualizações.
Em aplicativos Android, há um loop principal de atualização da UI assim:
Se a função é chamada durante a composição inicial ou em recomposições, dizemos que está presente na Composição.
Você pode inspecionar o layout do app gerado pelo Compose usando a ferramenta Layout Inspector do Android Studio,
Ferramenta Layout Inspector do Android Studio navegando para Tools > Layout Inspector.
Correções de layout para apresentação no emulador
Botão de decremento
Vamos mudar nossa pré-visualização para emular a largura comum de um celular pequeno, 320dp. Adicione um parâmetro widthDp
à anotação @Preview
.
Melhora layout
Uso do by
Aqui usamos uma palavra-chave by
ao invés do =
. Isso é um delegate de propriedade que poupa você de digitar .value
toda vez.
Adiciona parâmetros de mínimo e máximo
Adiciona CounterScreen com teste de vários contadores
Monta o layout de RGBCounterScreen
Inícia o State Hoisting do Counter
Em funções Composable, o estado que é lido ou modificado por várias funções deve existir em um ancestral comum — esse processo é chamado de State Hoisting, ou elevação de estado.
Como passamos eventos para cima? Passando callbacks para baixo. Callbacks são funções passadas como argumentos para outras funções e executadas quando o evento ocorre.
O padrão geral para elevação de estado no Jetpack Compose é substituir a variável de estado por dois parâmetros:
-
Valor: T - o valor atual a ser exibido;
-
OnValueChange: (T) -> Unit - um evento que solicita a mudança de valor com um novo valor T;
-
Propriedades importantes:
- Fonte única de verdade: ao mover o estado em vez de duplicá-lo, estamos garantindo que exista apenas uma fonte única de verdade. Isso ajuda a evitar bugs.
- Compartilhável: Estado elevado pode ser compartilhado com múltiplos composables.
- Interceptável: Chamadores dos composables sem estado podem decidir ignorar ou modificar eventos antes de mudar o estado.
- Desacoplado: O estado para uma função composable sem estado pode ser armazenado em qualquer lugar. Por exemplo, em um ViewModel.
Exemplo de uso do novo Stateless Counter
note
No Compose, você não esconde elementos da UI. Em vez disso, você simplesmente não os adiciona à composição, então eles não são incluídos na árvore de UI que o Compose gera. Você faz isso com simples lógica condicional em Kotlin.
Adicionar os limites ao formato Stateless
Versão funcional da RGBCounterScreen
Adiciona parâmetro step ao contador
Adiciona parâmetro text ao contador
Ideia simples para diminuir repetição de código
Atualiza mainactivity
important
Se você executar o aplicativo em um dispositivo, clicar nos botões e então girar, a tela será mostrada novamente. A função remember
funciona apenas enquanto o composable for mantido na Composição. Quando você gira, toda a atividade é reiniciada, então todo o estado é perdido. Isso também acontece com qualquer mudança de configuração e com a morte do processo.
Na próxima aula veremos como resolver esse problema.
06 - Gerenciamento de Estado e ViewModels
Código exemplo: Jogo da Velha
https://github.com/tads-ufpr-alexkutzke/ds151-example-tictactoe
Desenvolvimento da aplicação MyTasks
A seguir está o passo a passo do desenvolvimento da aplicação MyTasks
https://github.com/tads-ufpr-alexkutzke/ds151-aula-06-mytasks/
1. Criação do componente TaskItem
O componente TaskItem
exibe uma Tarefa.
Ele foi planejado para ser do tipo stateless, ou seja, seu estado será manipulado pelo seu antecessor.
Commit: TaskItem Layout - Diffs 17b16e81
TaskItem.kt
2. Criação do componente TaskList
O componente TaskItem
exibe uma lista de tarefas, ou seja, uma lista de TaskItem
.
Pontos importantes:
- Parâmetro
key
: para que aLazyColumn
mantenha um controle mais preciso de quais elementos foram atualizados, é importante informar uma forma de identificar cada elemento unicamente:- Aqui usamos um
id
criado de forma randômica para cadaTask
:val id: UUID = UUID.randomUUID()
;
- Aqui usamos um
- Também foi projetada para ser
stateless
;
Commit: TaskList - Diffs 1b615fd4
TaskList.kt
3. Criação dos componentes principal - MyTasks
O componente MyTasks
é responsável por abrigar outros dois componentes:
NewTaskControl
: caixa de texto e botão para a criação de uma nova tarefa;TaskList
: lista de tarefas;
Commit: NewTaskControl and MyTasks - Diffs a39ac305
MyTasks.kt
Componente NewTaskControl
O componente NewTaskControl
faz uso de um TextField
, mais precisamente, de um OutlinedTextField
.
Campos de texto precisam de, pelo menos, dois parâmetros:
value
: texto atual apresentado na caixa de texto;onValueChange
: evento de alteração do texto. É um callback com um argumento, o novo valor do texto;
NewTaskControl.kt
4. Inclusão dos primeiros estados
Nesse commit o componente MyTasks
recebe dois estados novos:
var newTaskText by remember { mutableStateOf("") }
var tasks = remember { mutableStateListOf<Task>() }
newTaskText
é o texto armazenado no TextField
.
tasks
é a lista de tarefas atual.
Commit: First states - Diffs bb906d1c
MyTasks.kt
Pontos importantes:
-
Problema com o
LazyColumn
e estados:- Se elemento sai da tela, ele perde o estado:
- Solução simples: utilizar
rememberSaveable
ao invés deremember
:rememberSaveable
consegue sobreviver a pequenas alterações na tela, mas não pode armazenar dados complexos como listas;
-
MutableList
s observáveis:- Utilizar
MutableStateListOf
outoMutableStateList()
- Utilizar
TaskList.kt
5. Implementação completa de estados
Para adicionar todo o funcionamento da tela, tornamos o valor checked
observável na classe Task
.
Assim, o componente TaskList
consegue recompor sempre que uma tarefa tem seu estado de checked
alterado.
Pontos importantes:
- Como os eventos
onCheckedChange
eonRemoveClick
são implementados.
Commit: Events and states working - Diffs 07479b85
MyTasks.kt
TaskList.kt
5. Implementação completa de estados
Apenas ordena as tarefas para apresentar as não realizas antes.
Commit: Sort tasks - Diffs 26f4dc53
MyTasks.kt
important
Na forma como está, qualquer alteração na tela, como rotacionar o celular, faz toda a lista de tarefas ser perdida.
6. Utilizando ViewModel
para armazenar estados
Commit: Implements MyTasksViewModel - Diffs c3d0f24b
Existem dois tipos de lógica:
- Logica de UI ou Comportamento: como exibir alterações no estado;
- Lógica de Negócios ou Domínio: o que fazer quando o estado é alterado;
A Lógica de Domínio é, geralmente, armazenada em outras camadas da aplicação (por exemplo, camada de dados, como veremos a seguir).
ViewModels permitem que composables
e outros componentes de UI acessem a lógica de domínio armazenadas em uma camada de dados da aplicação:
ViewModels
sobrevivem a mais eventos do quecomposables
:- Seguem o ciclo de vida de seu antecessor, por exemplo, da
Activity
; - Inclusive a trocas de telas, se assim for necessário;
- Seguem o ciclo de vida de seu antecessor, por exemplo, da
É uma boa prática manter as Lógicas de UI e de Domínio separadas: mover para uma ViewModel
;
Unidirectional data flow
Nesse commit, criamos uma nova ViewModel
, chamada MyTasksViewModel
:
MyTasksViewModel.kt
Para que o projeto compile corretamente, é necessário adicionar uma nova dependência ao arquivo app/build.gradle.kts
:
j
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
// ou (depende da versão do android studio)
implementation(libs.androidx.lifecycle.viewmodel.compose)
build.gradle.kts
Utilizando o ViewModel criado
Podemos acessar o MyTasksViewModel
em qualquer composable
, chamando viewModel()
.:
viewModel()
retorna umViewModel
existente ou cria um novo no escopo atual;
No componente MyTasks
adicionamos um parâmetro que chamado myTasksViewModel
que recebe como valor default o resultado de viewModel()
;
MyTasks.kt
myTasksViewModel
poderá ser permitirá ao composable acessar e manipular os dados desse ViewModel;
O ViewModel criado será mantido enquanto o seu escopo estiver vivo. Nesse caso, enquanto a Activity que possui MyTasks
estiver viva;
07 - Introdução à navegação no Jetpack Compose
Navegação é o processo de mover o usuário entre diferentes telas (ou funcionalidades) do App.
Com Jetpack Compose, isso é feito através de componentes baseados na ideia de UI declarativa. Ou seja, ainda utilizaremos Composables
.
A navegação com Jetpack Compose tem alguns pontos interessantes:
- Facilita a organização, controle e o fluxo entre telas em apps criados com Compose.
- Reduz acoplamento e gerencia automaticamente a pilha de telas (back stack).
Conceitos Básicos
NavHost
: Contêiner que exibe as telas (destinos) conforme o fluxo de navegação.NavController
: Controlador responsável por gerenciar comandos de navegação (ir para próxima tela, voltar, etc).NavGraph
: Também chamado de Composable Destinations, define as trocas de telas possíveis na aplicação.
important
Cada tela navegável é uma função @Composable.
Abordaremos cada um desses conceitos a seguir. Mas antes, é necessário configurar o projeto.
Configurando a Navegação
Basta adicionarmos uma dependência ao projeto para que as funções de navegação estejam disponíveis.
dependencies {
val nav_version = "2.8.9"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
Uma aplicação simples com navegação
Link para o repositório com o código completo: https://github.com/tads-ufpr-alexkutzke/ds151-aula-07-movies-app/tree/main
A aplicação proposta é bastante simples e terá apenas duas telas.
Em primeiro lugar, criaremos dois Composables para representar as telas da aplicação.
@Composable
fun MoviesScreen(onGoToMovieDetailsClick: () -> Unit = {}){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text="Tela de Filmes")
ElevatedButton(
onClick = onGoToMovieDetailsClick
) {
Text("Vai para tela de detalhes")
}
}
}
@Preview
@Composable
fun MoviesScreenPreview() {
MoviesAppTheme{
MoviesScreen()
}
}
@Composable
fun MovieDetailsScreen(){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text= "Tela de detalhes")
}
}
@Preview
@Composable
fun MovieDetailsPreview(){
MoviesAppTheme{
MovieDetailsScreen()
}
}
Os composables MoviesScreen
e MovieDetailsScreen
não tem nada de diferente dos composables vistos nas últimas aulas.
O componente MoviesApp
Agora criaremos um componente que irá representar a aplicação como um todo e será responsável por abrigar todos os dados referentes à navegação.
A criação desse componente não é obrigatória, mas é um padrão comum de desenvolvimento com o Jetpack Compose.
@Composable
fun MoviesApp(
navController: NavHostController = rememberNavController()
){
NavHost(
navController = navController,
startDestination = "movies",
){
composable("movies"){
MoviesScreen()
}
composable("movieDetails"){
MovieDetailsScreen()
}
}
}
@Preview
@Composable
fun MoviesAppPreview(){
MoviesAppTheme{}
MoviesApp()
}
}
No código acima, já temos os 3 componentes da navegação:
NavHost
: define um componente de navegação:navController
: variável vem derememberNavController()
;startDestination
: define a tela inicial para a navegação;
composable
ouNavGraph
: cadacomposable
define uma rota, ou tela, possível para a navegação:- Uma rota pode ser entendida como uma url;
- Existem várias formas de nomear uma rota: a mais simples é apenas com uma string contendo um nome único;
- A seguir veremos outras formas;
No exemplo, temos duas rotas: "movies"
e "movieDetails"
. Cada uma renderiza uma das telas criadas.
Já podemos atualizar a MainActivity
para que utilize nosso novo componente;
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MoviesAppTheme{}
MoviesApp()
}
}
}
}
@Composable
fun MoviesApp(
navController: NavHostController = rememberNavController(),
){
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
NavHost(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = "movies",
) {
composable("movies") {
MoviesScreen()
}
composable("movieDetails") {
MovieDetailsScreen()
}
}
}
}
No exemplo, já aproveitamos para passar o Scaffold
também para dentro do componente MoviesApp
.
Navegando para a segunda tela
Para trocarmos de tela, é necessário utilizarmos o método navigate
do componente navController
.
A tela MoviesScreen
já possui um evento para definir o comportamento do botão para trocar de tela.
Portanto, basta atribuir esse evento:
composable("movies") {
MoviesScreen(
onGoToMovieDetailsClick = {
navController.navigate("movieDetails")
})
}
Dessa forma, o botão já realizar a troca de telas.
Um ponto interessante, é que o NavHost
já irá cuidar de toda a pilha de telas automaticamente. Por exemplo, ao rodar a aplicação no emulador, o botão de Voltar
da interface do smartphone já é capaz de retornar à tela anterior.
Porém, também é possível adicionarmos um botão na tela movieDetails
para realizarmos a volta para a tela anterior.
important
É importante que a lógica de navegação não seja compartilhada entre os componentes. Ou seja, os componentes filhos (telas e outros) devem apenas utilizar funções definidas no escopo do NavHost
. Por exemplo, não é uma boa prática, passar a variável navController
como parâmetro para outros componentes.
Primeiro, adicionamos o botão:
@Composable
fun MovieDetailsScreen(
onGoBackClick: () -> Unit = {}
){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text= "Tela de detalhes")
ElevatedButton(
onClick = onGoBackClick
) {
Text("Voltar")
}
}
}
Agora, basta atribuir o evento corretamente no componente MoviesApp
:
composable("movieDetails") {
MovieDetailsScreen(
onGoBackClick = {
navController.navigate("movies")
}
)
}
Passando argumentos para telas
Ao trocarmos de tela, por vezes, é interessante que argumentos sejam passados. Por exemplo, ao trocar para a tela de detalhes de um filme, talvez seja interessante informarmos o id
do filme a ser mostrado na tela seguinte.
Para realizar a passagem desses argumentos, pode alterar um pouco a rota:
composable("movies") {
MoviesScreen(
onGoToMovieDetailsClick = {
navController.navigate("movieDetails/10")
})
}
composable("movieDetails/{movieId}") { backStackEntry ->
val movieId:String = backStackEntry.arguments?.getString("movieId") ?: ""
MovieDetailsScreen(
movieId = movieId,
onGoBackClick = {
navController.navigate("movies")
}
)
}
// ...
@Composable
fun MovieDetailsScreen(
movieId: String = "",
onGoBackClick: () -> Unit = {}
){
// ...
Text(text= "Tela de detalhes")
Text(text= "MovieId: $movieId")
// ...
}
É preciso prestar um pouco de atenção aos tipos de variáveis. Como um determinado argumento pode ser nulo, a variável que o recebe deve ser nullable
, ou uma checagem deve ser realizada antes (operador ?:
).
Definindo tipos de argumentos
É possível, ainda, definir mais detalhes sobre os argumentos passados. Para isso, utilizaremos o parâmetro argumentos
e o componente navArgument
:
composable(
route="movieDetails/{movieId}",
arguments = listOf(
navArgument("movieId") {
defaultValue = 0
type = NavType.IntType
}
)
) { backStackEntry ->
val movieId:Int? = backStackEntry.arguments?.getInt("movieId")
movieId?.let{
MovieDetailsScreen(
movieId = it,
onGoBackClick = {
navController.navigate("movies")
}
)
}
}
}
// ...
@Composable
fun MovieDetailsScreen(
movieId: Int = -1,
onGoBackClick: () -> Unit = {}
){
// ...
Text(text= "Tela de detalhes")
Text(text= "MovieId: $movieId")
// ...
}
Outras formas de retornar na navegação
Navegação aninhada
Referência: https://medium.com/@KaushalVasava/navigation-in-jetpack-compose-full-guide-beginner-to-advanced-950c1133740
Destinações podem ser agrupadas em um grafo mais complexo, ou aninhado:
Para isso, agrupe rotas relacionadas em um componente navigation
:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
08 - Integração com APIs e Consumo de dados
Nessa aula veremos como realizar requisições HTTP em uma aplicação Android com Compose.
Para isso, utilizaremos algumas bibliotecas auxiliares e trabalharemos com o conceito de Coroutines
do Kotlin.
O material abaixo é baseado na criação de um aplicativo que acessa a API https://moviesapi.kutzke.com.br/movies. Essa API simples lista os 40 filmes mais bem avaliados do IMDB.
A versão final da aplicação pode ser acessada no seguinte repositório:
https://github.com/tads-ufpr-alexkutzke/ds151-aula-08-movies-api-app/
A seguir, observaremos as alterações realizadas commit a commit para compreender o funcionamento das requisições HTTP e outros recursos.
1. Commit: first commit - basic app structure - Diffs 290f5aad
2. Commit: api first version - Diffs 1f445d03
O primeiro passo é adicionar as dependências necessárias (Navigation, ViewModel, Retrofit, okHttp):
build.gradle.kts
É necessários também, informar ao Android que nosso aplicativo precisa de permissão para acessar a internet. Para isso, adicione o seguinte ao AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
Além disso, caso precise acessar uma API por HTTP, e não HTTPs, adicione o seguinte parâmetro ao bloco application
do mesmo arquivo:
<application
android:usesCleartextTraffic="true"
Vale observar, também, que o Android, por questões de segurança, não permite acesso ao localhost
. Portanto, para acessar uma API que está na seu próprio computador, utilize o IP da rede local.
AndroidManifest.xml
Configuração do Retrofit
A biblioteca Retrofit irá realizar todo o trabalho de acesso à API e conversão dos dados. Para isso, é comum colocarmos toda configuração de acesso à API (inicialização do Retrofit) em um arquivo separado. Nesse projeto, criamos o arquivo network/MoviesApiService.kt
.
MoviesApiService.kt
Nesse arquivo, definimos a URL base do servidor que acessaremos. No código acima, ela estava definida para meu IP local, porém, podemos atualizá-la para o servidor da API movieisapi
:
private const val BASE_URL =
"https://movieisapi.kutzke.com.br/"
O trecho abaixo, mostra a configuração básica do Retrofit. Nele, passamos a URL base e determinamos um ConverterFactory
. É esse objeto que será responsável por converter a resposta em algum tipo de dado desejado. Nesse caso, GsonConverterFactory
irá converter o formato JSON entregue pela API em um objeto acessível pelo Kotlin (nesse caso, List<Movie>
).
private val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
Na sequência, se determina os endpoints acessível pela aplicação. Para isso, definimos uma interface e adicionamos uma função suspensa para cada endpoint, com os devidos marcadores de método (@GET
) fornecidos pelo Retrofit:
interface MoviesApiService {
@GET("/movies")
suspend fun getMovies(): List<Movie>
}
Por fim, define-se um object
que será utilizado pelo resto do App para acessar a API. Esse object contem apenas uma variável que é inicializada por retrofit.create
.
Aqui, o uso de object
e lazy
tem uma razão de desempenho. Object irá garantir que apenas um objeto da classe MoviesApi
será criado em toda aplicação. e o lazy
determina que retrofitService
só será inicializado quando for requisitado pela primeira vez. A inicialização do retrofit é uma operação cara, por isso deve ser feita apenas uma vez.
object MoviesApi {
val retrofitService: MoviesApiService by lazy {
retrofit.create(MoviesApiService::class.java)
}
}
Utilizando MoviesApi no ViewModel
Embora MoviesApi
estará disponível em qualquer lugar da aplicação, geralmente reservamos seu uso para dentro de um ViewModel
. Desse modo, para outros componentes da aplicação, o acesso aos dados fica transparente.
MoviesAppViewModel.kt
O código acima tem alguns pontos interessantes.
Primeiro, a definição do estado, que nesse caso, será uma lista de filmes:
private var _movies = mutableStateListOf<Movie>()
val movies: List<Movie>
get() = _movies
Na sequência, o construtor do ViewModel
faz uso de um viewModelScope
para realizar as chamadas assíncronas à API:
init {
if(fake) _movies.addAll(fourMovies)
else{
viewModelScope.launch {
val movies = MoviesApi.retrofitService.getMovies()
_movies.addAll(movies)
}
}
}
É importante salientar que, no exemplo acima, não fazemos nenhum tratamento de erro.
O viewModelScope.launch
utilizado no construtor é algo novo. Ele determina o início de uma nova Coroutine, permitindo a realização de operações assíncronas e paralelas forma simples e segura (do ponto de vista de programação).
Sobre Coroutines
Coroutines (ou corrotinas) em Kotlin são uma utilizados para a programação de código assíncrono e concorrente de forma mais simples e eficiente. Elas permitem que realizar operações potencialmente demoradas — como acessar a internet ou ler arquivos — sem bloquear a thread principal (geralmente a de interface do usuário), tornando aplicativos mais responsivos.
Uma coroutine é, basicamente, uma sequência de instruções que pode ser suspensa e retomada posteriormente, permitindo que outras tarefas sejam executadas enquanto ela aguarda alguma operação, como uma resposta de rede ou leitura de dados. Isso é feito de simples e estruturada, sem a complexidade tradicional de manipular múltiplas threads diretamente.
Kotlin oferece bibliotecas robustas para o uso de coroutines, com funções especiais como suspend
, launch
, async
e delay
que tornam o gerenciamento de tarefas assíncronas muito mais legível. Ao invés de callbacks encadeados ou código difícil de manter, o uso de coroutines proporciona uma sintaxe sequencial que é mais fácil de entender e depurar. Além disso, as coroutines incorporam o conceito de concorrência estruturada, promovendo o controle do ciclo de vida das tarefas assíncronas para evitar vazamentos de memória e garantir que todas as operações iniciadas sejam corretamente finalizadas ou canceladas quando necessário.
Considere o código a seguir (exemplos retirados de https://developer.android.com/codelabs/basic-android-kotlin-compose-coroutines-kotlin-playground):
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
delay(1000)
println("Sunny")
}
}
-
runBlocking()
executa um loop de eventos, que pode lidar com várias tarefas ao mesmo tempo, continuando cada tarefa de onde parou quando ela está pronta para ser retomada. -
delay()
é na verdade uma função especial de suspensão fornecida pela biblioteca de coroutines do Kotlin.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
printForecast()
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
-
Uma função suspensa é como uma função normal, mas pode ser suspensa e retomada novamente mais tarde.
-
Uma função suspensa só pode ser chamada a partir de uma coroutine ou de outra função suspensa.
-
Um ponto de suspensão é o local dentro da função onde a execução pode ser suspensa.
-
O "co-" em coroutine significa cooperativo. O código coopera para compartilhar o loop de eventos subjacente ao suspender a execução enquanto espera por algo, o que permite que outros trabalhos sejam executados nesse meio tempo.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
launch {
printForecast()
}
launch {
printTemperature()
}
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
suspend fun printTemperature() {
delay(1000)
println("30\u00b0C")
}
- A função
launch()
da biblioteca de coroutines inicia uma nova coroutine. - Coroutines em Kotlin seguem um conceito chave chamado concorrência estruturada, onde o seu código é sequencial por padrão e coopera com um loop de eventos subjacente, a menos que você peça explicitamente para executar concorrentemente (por exemplo, usando
launch()
).
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
val forecast: Deferred<String> = async {
getForecast()
}
val temperature: Deferred<String> = async {
getTemperature()
}
println("${forecast.await()} ${temperature.await()}")
println("Have a good day!")
}
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
- A função
async()
é utilizada quando é importante determinarmos o momento em que a coroutine termina e precisa de um valor de retorno dela. - A função
async()
retorna um objeto do tipoDeferred
, que funciona como uma promessa de que o resultado estará disponível quando estiver pronto. Você pode acessar o resultado no objetoDeferred
usandoawait()
.
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
-
coroutineScope{}
cria um escopo local. -
As coroutines iniciadas dentro deste escopo são agrupadas juntas dentro deste escopo, o que tem implicações para cancelamento e exceções.
-
coroutineScope()
só retornará quando todo o seu trabalho, incluindo quaisquer coroutines iniciadas, for concluído. -
Com
coroutineScope()
, mesmo que a função execute internamente trabalhos de forma concorrente, para quem chama parece uma operação síncrona, porquecoroutineScope
não retorna até todo o trabalho estar completo. -
launch()
easync()
são funções de extensão emCoroutineScope
. Chamelaunch()
ouasync()
no escopo para criar uma nova coroutine dentro desse escopo. -
Um
CoroutineScope
está atrelado a um ciclo de vida, que define quanto tempo as coroutines dentro desse escopo viverão. Se um escopo for cancelado, seu job também é cancelado, propagando o cancelamento para seus filhos. Se um job filho falha com exceção, os outros jobs filhos são cancelados, o job pai é cancelado e a exceção é relançada para o chamador. -
O Android fornece suporte a escopo de coroutines em entidades que têm um ciclo de vida bem definido, como
Activity
(lifecycleScope
) eViewModel
(viewModelScope
). -
Coroutines iniciadas dentro desses escopos obedecerão ao ciclo de vida da entidade correspondente, como
Activity
ouViewModel
.
important
Aplicações Android geralmente possuem uma "Main thread" que é responsável pela atualização e renderização da tela. Sem o uso de coroutines, como viewModelScope
, a tela ficaria "travada" até que processamentos assíncronos ou mais longos terminassem.
3. Commit: MovieDetails with api request - Diffs a0830030
Nesse commit, atualizamos a tela MovieDetailsScreen
para acessar os dados de um filme por meio de um novo endpoint: /movies/{movieId}
.
Novo endpoint com parâmetro
Adicionamos as informações para o novo endpoint, com o detalhe de que ele espera um movieId
como parâmetro:
@GET("/movies/{id}")
suspend fun getMovie(@Path("id") id:Int): Movie
MoviesApiService.kt
Atualização do MoviesAppViewModel
Devemos, também, atualizar o MoviesAppViewModel
para que ele acesse esse novo endpoint.
Dois detalhes importantes estão nessa atualização.
Em primeiro lugar, definimos um novo estado movieDetails
, do tipo StateFlow<Movie?>
. StateFlow
são uma forma comum de armazenar estados mais complexos como classes. Flow
é um conceito do kotlin e tem relação com o paradigma Produtor-Coletor. Mas, para nós, no momento, será apenas uma forma conveniente de armazenar estados.
private var _movieDetails = MutableStateFlow<Movie?>(null)
val movieDetails: StateFlow<Movie?> = _movieDetails
Em segundo lugar, alteramos a função getMovie
, para que ela utilize o novo endpoint. Aqui adicionamos uma chamada para delay
apenas para que fique claro que um processamento assíncrono está ocorrendo.
Vale notar a necessidade de utilizar .value
para acessar o valor do estado quando trabalhamos com StateFlow
.
fun getMovie(movieId:Int) {
viewModelScope.launch {
delay(2000)
val movie = MoviesApi.retrofitService.getMovie(movieId)
_movieDetails.value = movie
}
}
Uma pequena alteração foi feita para permitir o uso de Previews, uma vez que o acesso a API só ocorre na execução pelo emulador e dispositivo físico:
init {
if(fake) _movies.addAll(fourMovies)
else{
MoviesAppViewModel.kt
Uso do novo endpoint
Para recuperar o estado do moviesAppViewModel
, utilizamos o método collectAsState
, uma vez que estamos utilizando um StateFlow
.
val movie = moviesAppViewModel.movieDetails.collectAsState()
Agora, utilizamos um LaunchedEffect
para invocar a função getMovie
do moviesAppViewModel
.
Um LaunchedEffect
é um recurso utilizado em Composables para lidar com efeitos colaterais. Se for necessário executar algum código que não segue a mesma linha de execução do composable, devemos separá-lo. Por exemplo, um código assíncrono ou que leva muito tempo, são efeitos colaterais.
Nesse caso, o bloco LaunchedEffect
será executado de forma segura nos momentos necessários. Esse bloco será reexecutado sempre que um dos seus parâmetros tiver seu valor alterado (no caso, movieId
). Ou seja, ele não é executado sempre que uma recomposição ocorre.
LaunchedEffect(movieId) {
moviesAppViewModel.getMovie(movieId)
}
Basta, agora, renderizar o composable MovieDetailsScreen
quando o estado movie
não for nulo:
if(movie.value == null) Text("Carregando ...")
else{
movie.value?.let {
MovieDetailsScreen(
movie = it,
onGoBackClick = {
navController.navigate("movies")
}
)
}
}
MoviesApp.kt
4. Commit MovieDetailsScreen handles api - Diffs 42a1de57
Um detalhe importante da última atualização é o fato de que o componente MoviesApp
está cuidando da chamada para a API. Isso gera o inconveniente de utilizarmos LaunchedEffect
dentro de um composable
do NavHost
. Esses componentes de navegação devem ser simples e tratar apenas de lógicas de navegação.
Nesse caso, portanto, podemos passar o tratamento de chamadas API para o componente que necessita dessas informações, MovieDetailsScreen
.
note
Para essa alteração, o componente MovieDetailsScreen
precisará acessar moviesAppViewModel
. A decisão de passar viewModels
para componentes deve ser considerada com cuidado. Ela pode complexificar o código e tornar a testagem mais difícil. Porém, nesse caso, moviesAppViewModel
será acessado apenas pelo componente MovieDetailsScreen
e não por seus componentes internos.
Simplificando MoviesApp
No componente de navegação MoviesApp
as alterações são simples. Basta passar toda a lógica de API para o componente MovieDetailsScreen
que receberá, agora o movieId
, apenas.
val movieId:Int? = backStackEntry.arguments?.getInt("movieId")
if(movieId == null) Text("Carregando ...")
else{
movieId.let {
MovieDetailsScreen(
movieId = it,
onGoBackClick = {
navController.popBackStack()
}
)
}
MoviesApp.kt
Acessando API no componente MovieDetailsScreen
Algumas alterações são necessárias para que a tela MovieDetailsScreen
possa lidar com requisições de API.
Inicialmente, ela precisa ter acesso ao viewModel, uma vez que todas as chamadas para API são controladas por moviesAppViewModel
. Portanto, adicionamos um parâmetro para a tela que recebe, por padrão o valor de viewModel()
.
@Composable
fun MovieDetailsScreen(
movieId: Int,
moviesAppViewModel: MoviesAppViewModel = viewModel(),
onGoBackClick: () -> Unit = {},
){
O restante do composable é, basicamente, o que estava em MoviesApp
anteriormente:
@Composable
fun MovieDetailsScreen(
// ...
val movie = moviesAppViewModel.movieDetails.collectAsState()
LaunchedEffect(movieId) {
moviesAppViewModel.getMovie(movieId)
}
if(movie.value == null) Text("Carregando ...")
movie.value?.let{ movie ->
MovieDetailsScreen(movie = movie)
}
}
Um outro detalhe interessante é a criação de um outro componente com o mesmo nome, mas com uma assinatura de parâmetros diferente:
@Composable
fun MovieDetailsScreen(
movie: Movie = fourMovies[0],
onGoBackClick: () -> Unit = {},
){
MovieItem(movie = movie)
}
Isso permite a criação mais simples de um Preview, uma vez que, como sabemos, não podem realizar requisições HTTP ou operações assíncronas.
@Preview
@Composable
fun MovieDetailsScreenPreview(){
MoviesAppTheme {
MovieDetailsScreen(
movie = fourMovies[0]
)
}
}
Assim, criamos um preview para a instância de MovieDetailsScreen
que não necessita da lógica de API.
MovieDetailsScreen.kt
5. Commit: Exception handling and UiState - Diffs 68567bdd
Para finalizar o desenvolvimento dessa aplicação inicial, vamos adicionar tratamento de exceções para que problemas de acesso à API não causem respostas inesperadas ao usuário.
Adicionando mais estados para tratamento de exceções
Quando realizamos requisições a uma API, podemos interpretar que a interface pode ter três estados principais (cada um com seus dados específicos): sucesso, erro e carregando.
Portanto, podemos alterar os estados armazenados em MoviesAppViewModel
para que atendam essa nova organização.
Existem muitas formas de fazer isso. Uma delas é definindo uma sealed interface
:
sealed interface MoviesScreenUiState {
class Success(val movies: List<Movie>): MoviesScreenUiState
object Error: MoviesScreenUiState
object Loading: MoviesScreenUiState
}
sealed interface MovieDetailsScreenUiState {
class Success(val movie: Movie ) : MovieDetailsScreenUiState
object Error : MovieDetailsScreenUiState
object Loading : MovieDetailsScreenUiState
}
Aqui, embora estejamos no mesmo viewModel, definimos uma interface para o estado de cada tela. Isso não é sempre necessário, mas, no caso da aplicação aqui apresentada, é uma solução mais simples.
Como os itens Error
e Loading
não possuem dados, eles podem ser declarados como object
e não class
.
Agora, o MoviesAppViewModel
pode utilizar essas interfaces para gerar novos estados:
class MoviesAppViewModel(val fake: Boolean = false): ViewModel() {
var moviesScreenUiState: MoviesScreenUiState by mutableStateOf(MoviesScreenUiState.Loading)
var movieDetailsScreenUiState: MovieDetailsScreenUiState by mutableStateOf(MovieDetailsScreenUiState.Loading)
Inicializamos os estamos para cada tela, por padrão, como .Loading
.
A lógica de carregamento da lista de filmes pode adicionar um bloco try
catch
para o tratamento de qualquer erro no acesso à API:
init {
getMovies()
}
private fun getMovies(){
viewModelScope.launch {
moviesScreenUiState = try {
val movies = MoviesApi.retrofitService.getMovies()
MoviesScreenUiState.Success(movies = movies)
}
catch(e: IOException){
MoviesScreenUiState.Error
}
}
}
Perceba que, dependendo do caso, atribuímos o valor diferente para moviesScreenUiState
. É como se os valores Success
, Error
e Loading
fossem passados como informação adicional ao objeto do estado.
A função getMovie
tem um comportamento muito parecido:
fun getMovie(movieId:Int) {
movieDetailsScreenUiState = MovieDetailsScreenUiState.Loading
viewModelScope.launch {
movieDetailsScreenUiState = try{
delay(2000)
val movie = MoviesApi.retrofitService.getMovie(movieId)
MovieDetailsScreenUiState.Success(movie = movie)
}
catch(e: IOException) {
MovieDetailsScreenUiState.Error
}
}
}
Um detalhe especial é que, a cada chamada de getMovie
reinicializamos o estado da tela com .Loading
para que um novo carregamento seja representado ao usuário.
MoviesAppViewModel.kt
Utilizando o novo estado nas telas
Os componentes de tela, MoviesScreen
e MovieDetailsScreen
precisam, apenas, utilizar esses novos estados.
Para simplificar, optamos a passagem desses estados como parâmetro para cada tela.
O desenho da tela pode ser realizado com um bloco when
, para cada tipo possível de valor de MoviesScreenUiState
:
fun MoviesScreen(
moviesScreenUiState: MoviesScreenUiState,
onGoToMovieDetailsClick: (movieId:Int) -> Unit = {},
){
when(moviesScreenUiState){
is MoviesScreenUiState.Success -> {
MoviesList(
movies = moviesScreenUiState.movies,
onMovieClick = onGoToMovieDetailsClick
)
}
is MoviesScreenUiState.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
is MoviesScreenUiState.Error -> ErrorScreen( modifier = Modifier.fillMaxSize())
}
}
Para a tela MovieDetailsScreen
as alterações são bastante semelhantes:
@Composable
fun MovieDetailsScreen(
movieId: Int,
moviesAppViewModel: MoviesAppViewModel = viewModel(),
movieDetailsScreenUiState: MovieDetailsScreenUiState,
onGoBackClick: () -> Unit = {},
){
when(movieDetailsScreenUiState){
is MovieDetailsScreenUiState.Success -> {
MovieDetailsScreen(movie = movieDetailsScreenUiState.movie)
}
is MovieDetailsScreenUiState.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
is MovieDetailsScreenUiState.Error -> ErrorScreen( modifier = Modifier.fillMaxSize())
}
LaunchedEffect(movieId) {
moviesAppViewModel.getMovie(movieId)
}
}
Aqui, ainda precisamos do acesso ao moviesAppViewModel
pois é necessário realizar a chamada à getMovie
.
MoviesScreen.kt
MovieDetailsScreen.kt
Ajustes em MoviesApp
Por fim, alguns pequenos ajustes são necessários no componente de navegação MoviesApp
.
composable("movies") {
MoviesScreen(
moviesScreenUiState = moviesAppViewModel.moviesScreenUiState,
onGoToMovieDetailsClick = { movieId ->
navController.navigate("movieDetails/$movieId")
}
)
}
Adicionamos o parâmetro moviesScreenUiState
e passamos o valor atual presente em moviesAppViewModel
.
Para MovieDetailsScreen
, as alterações são semelhantes:
composable(
route="movieDetails/{movieId}",
arguments = listOf(
navArgument ("movieId") {
defaultValue = 0
type = NavType.IntType
}
)
) { backStackEntry ->
val movieId:Int? = backStackEntry.arguments?.getInt("movieId")
movieId?.let {
MovieDetailsScreen(
movieId = it,
moviesAppViewModel = moviesAppViewModel,
movieDetailsScreenUiState = moviesAppViewModel.movieDetailsScreenUiState,
onGoBackClick = {
navController.popBackStack()
}
)
}
}
MoviesApp.kt
Atividade prática
Altere a tela MovieDetailsScreen
para que ela carregue e apresente reviews do filme escolhido.
A api possui um endpoint com reviews dos filmes http://moviesapi.kutzke.com.br/reviews.
O servidor da API utiliza Json-server. Consulte a documentação oficial para saber quais opções estão disponível (filtragem, paginação, etc).
09 - Persistência de dados local com Room
Room
O Room é uma biblioteca de persistência de dados do Android que facilita o armazenamento e o gerenciamento de dados locais em bancos de dados SQLite.
Ele fornece uma camada de abstração sobre o SQLite, permitindo que os desenvolvedores definam entidades, consultem e manipulem dados utilizando objetos e métodos Java/Kotlin de forma simples, segura e eficiente.
Com o Room, é possível manter o código mais organizado, evitar erros comuns de SQL e aproveitar recursos como verificação de consultas em tempo de compilação e integração direta com o ciclo de vida do aplicativo.
Componentes Principais do Room
O Room possui três componentes principais:
- Classe de banco de dados: Gerencia o banco de dados e é o principal ponto de acesso para os dados persistidos do aplicativo.
- Entidades de dados: Representam as tabelas do banco de dados do app.
- DAOs (Data Access Objects): Fornecem métodos para consultar, atualizar, inserir e deletar dados no banco.
A classe de banco de dados fornece instâncias dos DAOs, que permitem ao app acessar e manipular os dados como objetos correspondentes às entidades definidas. Dessa forma, o app pode recuperar, inserir ou atualizar informações nas tabelas do banco com facilidade.
Exemplo simples
Entidades
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Uso básico
Primeiro precisamos gerar o objeto da Database:
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
Na sequência, utilizamos a instâncias db
para acessar os dados através das DAOs disponíveis:
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Aplicação exemplo: MoviesApp
A versão final da aplicação pode ser acessada no seguinte repositório:
https://github.com/tads-ufpr-alexkutzke/ds151-aula-09-movies-api-room-app
A aplicação exemplo segue o desenvolvimento da última aula, com a adição de armazenamento local com Room. O objetivo da aplicação é que o usuário possa salvar localmente os dados dos filmes marcados como favoritos. Esses dados devem ser acessíveis mesmo sem acesso à internet.
A organização dos arquivos nessa aplicação foi melhorada. Agora temos arquivos separados em pastas que indicam suas funções. A estrutura é a seguinte:
app/src/main/java/com/example/moviesapp/
├── AppContextHolder.kt
├── Application.kt
├── data
│ ├── local
│ │ ├── CastConverter.kt
│ │ ├── FavoriteMovieDao.kt
│ │ ├── FavoriteMovieEntity.kt
│ │ └── FavoriteMoviesDatabase.kt
│ ├── LocalFavoriteMoviesRepository.kt
│ ├── LocalFavoriteMoviesRepositoryProvider.kt
│ ├── RemoteMoviesRepository.kt
│ └── RemoteMoviesRepositoryProvider.kt
├── MainActivity.kt
├── model
│ ├── Movie.kt
│ └── Review.kt
├── network
│ └── MoviesApiService.kt
├── ui
│ ├── moviesapp
│ │ ├── ApiComposables.kt
│ │ ├── MovieDetailsScreen.kt
│ │ ├── MovieDetailsViewModel.kt
│ │ ├── MovieItem.kt
│ │ ├── MoviesList.kt
│ │ ├── MoviesListViewModel.kt
│ │ └── MoviesScreen.kt
│ ├── MoviesApp.kt
│ └── theme
│ ├── Color.kt
│ ├── Theme.kt
│ └── Type.kt
└── utils
└── MapperExtensions.kt
O conteúdo de cada pasta é o seguinte:
data/
: arquivos relativos a repositórios de dados, sejam locais ou remotos. Aqui armazenamos os itens conhecidos como Repositórios;data/local/
: arquivos relativos ao armazenamento local de dados (Database, DAO e Entity);
model
: arquivos que definem os modelos dos dados utilizados. Em geral, determinam os tipos de dados relevantes para a aplicação;network/
: arquivos relacionados à requisições para APIs. No caso, arquivos de definição doRetrofit
;ui/
arquivos da interface (Composables
eViewModels
);utils/
: arquivos auxiliares;
important
Nesse projeto, utilizamos uma paradigma de repositórios para abstrair todo o acesso aos dados da aplicação. Por essa razão, uma certa quantidade de código boilerplate precisou ser adicionada (vide os arquivos de providers). Esse efeito pode ser sensivelmente diminuído através do uso de Injeção de Dependências (DI - Dependencies Injection). No desenvolvimento Android, isso geralmente é realizado com o auxílio da biblioteca Hilt.
Dependências
Algumas dependências são necessárias para o uso do Room (app/build.gradle.kts
):
plugins {
// ...
id("com.google.devtools.ksp")
}
dependencies {
// ...
val room_version = "2.7.1"
implementation("androidx.room:room-runtime:$room_version")
ksp("androidx.room:room-compiler:$room_version")
// ...
}
Atenção ao plugin ksp
adicionado. Além disso, esse plugin deve ser adicionado, também, ao arquivo build.gradle.kts
do projeto:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
Model
Antes de abordarmos a implementação do acesso aos dados locais, devemos conhecer quais são os tipos de dados utilizados na lógica da aplicação
Movie.kt
Define a classe Movie:
class Movie(
val id: Int,
val title: String,
val cast: List<String>,
val director: String,
val synopsis: String,
val posterUrl: String,
) {
}
Movie.kt
package com.example.moviesapp.model
class Movie(
val id: Int,
val title: String,
val cast: List<String>,
val director: String,
val synopsis: String,
val posterUrl: String,
) {
}
val fourMovies: List<Movie> = listOf(
Movie(
id = 1,
title = "Um Sonho de Liberdade",
cast = listOf("Tim Robbins", "Morgan Freeman", "William Sadler"),
director = "Frank Darabont",
synopsis = "Acusado injustamente de assassinato, Andy Dufresne encontra esperança e redenção na prisão de Shawshank.",
posterUrl = "https://image.tmdb.org/t/p/w500/q6y0Go1tsGEsmtFryDOJo3dEmqu.jpg",
),
Movie(
id = 2,
title = "O Poderoso Chefão",
cast = listOf("Marlon Brando", "Al Pacino", "James Caan"),
director = "Francis Ford Coppola",
synopsis = "A trajetória da família mafiosa Corleone e os desafios enfrentados no submundo do crime.",
posterUrl = "https://image.tmdb.org/t/p/w500/d4KNaTrltq6bpkFS01pYtyXa09m.jpg",
),
Movie(
id = 3,
title = "O Poderoso Chefão II",
cast = listOf("Al Pacino", "Robert De Niro", "Robert Duvall"),
director = "Francis Ford Coppola",
synopsis = "Expande a saga dos Corleone, explorando passado e presente da família mafiosa.",
posterUrl = "https://image.tmdb.org/t/p/w500/amvmeQWheahG3StKwIE1f7jRnkZ.jpg",
),
Movie(
id = 4,
title = "Batman: O Cavaleiro das Trevas",
cast = listOf("Christian Bale", "Heath Ledger", "Aaron Eckhart"),
director = "Christopher Nolan",
synopsis = "O Coringa ameaça destruir Gotham, e Batman precisa lidar com caos e sacrifícios pessoais.",
posterUrl = "https://image.tmdb.org/t/p/w500/qJ2tW6WMUDux911r6m7haRef0WH.jpg",
),
)
Review.kt
Define a classe Review:
class Review(
val id: Int,
val movieId: Int,
val author: String,
val reviewText: String,
val rating: Int,
) {
}
Review.kt
package com.example.moviesapp.model
class Review(
val id: Int,
val movieId: Int,
val author: String,
val reviewText: String,
val rating: Int,
) {
}
val fourReviews: List<Review> = listOf(
Review(
id = 1,
movieId = 1,
author = "João Silva",
reviewText = "Uma história inspiradora sobre esperança, amizade e redenção. Atuações impecáveis de Tim Robbins e Morgan Freeman.",
rating = 10
),
Review(
id = 2,
movieId = 1,
author = "Ana Souza",
reviewText = "O filme emociona do começo ao fim, com uma narrativa envolvente e um final perfeito.",
rating = 9
),
Review(
id = 3,
movieId = 1,
author = "Carlos Mendes",
reviewText = "Roteiro cativante e personagens profundos. Um clássico absoluto do cinema.",
rating = 10
),
)
Data
Analisaremos os arquivos presentes na pasta data/
do projeto. São, portanto, responsáveis por toda definição, acesso e abstração dos dados utilizados na aplicação.
Data/Local
Os arquivos presentes em data/local
tem a função de definir e implementar o acesso aos dados locais, por meio do uso do Room.
FavoriteMovieEntity.kt
Este arquivo define a Entity que representa um filme favorito na base de dados local:
@Entity(tableName = "favorite_movies")
@TypeConverters(CastConverter::class)
data class FavoriteMovieEntity(
@PrimaryKey val id: Int,
val title: String,
val cast: List<String>,
val director: String,
val synopsis: String,
val posterUrl: String
)
FavoriteMovieEntity.kt
package com.example.moviesapp.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
@Entity(tableName = "favorite_movies")
@TypeConverters(CastConverter::class)
data class FavoriteMovieEntity(
@PrimaryKey val id: Int,
val title: String,
val cast: List<String>,
val director: String,
val synopsis: String,
val posterUrl: String
)
FavoriteMovieDao.kt
Agora temos a definição do DAO, responsável pelo acesso às entidades FavoriteMovieEntity
na base de dados local:
@Dao
interface FavoriteMovieDao {
@Query("SELECT * FROM favorite_movies")
suspend fun getAll(): List<FavoriteMovieEntity>
@Query("SELECT * FROM favorite_movies WHERE id = :movieId")
suspend fun getById(movieId: Int): FavoriteMovieEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(movie: FavoriteMovieEntity)
@Delete
suspend fun delete(movie: FavoriteMovieEntity)
}
FavoriteMovieDao.kt
package com.example.moviesapp.data.local
import androidx.room.*
@Dao
interface FavoriteMovieDao {
@Query("SELECT * FROM favorite_movies")
suspend fun getAll(): List<FavoriteMovieEntity>
@Query("SELECT * FROM favorite_movies WHERE id = :movieId")
suspend fun getById(movieId: Int): FavoriteMovieEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(movie: FavoriteMovieEntity)
@Delete
suspend fun delete(movie: FavoriteMovieEntity)
}
FavoriteMoviesDatabase
Por fim, temos a definição da própria base de dados local, com FavoriteMoviesDatabase
:
@Database(entities = [FavoriteMovieEntity::class], version = 1)
@TypeConverters(CastConverter::class)
abstract class FavoriteMoviesDatabase : RoomDatabase() {
abstract fun favoriteMovieDao(): FavoriteMovieDao
companion object {
@Volatile private var INSTANCE: FavoriteMoviesDatabase? = null
fun getInstance(context: Context): FavoriteMoviesDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
FavoriteMoviesDatabase::class.java,
"favorites.db"
).build().also { INSTANCE = it }
}
}
}
}
Aqui vale notar como a estrutura de definição de um Database está mais complexa do que no exemplo inicial deste material.
Isso se deve ao fato de que o código acima garante que apenas um objeto de FavoriteMoviesDatabase
seja instanciado para toda
a aplicação, não importando quantas vezes getInstance
seja chamado.
Perceba, porém, que o código central ainda é uma simples chamada para Room.databaseBuilder
:
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
FavoriteMoviesDatabase::class.java,
"favorites.db"
).build().also { INSTANCE = it }
FavoriteMoviesDatabase.kt
package com.example.moviesapp.data.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(entities = [FavoriteMovieEntity::class], version = 1)
@TypeConverters(CastConverter::class)
abstract class FavoriteMoviesDatabase : RoomDatabase() {
abstract fun favoriteMovieDao(): FavoriteMovieDao
companion object {
@Volatile private var INSTANCE: FavoriteMoviesDatabase? = null
fun getInstance(context: Context): FavoriteMoviesDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
FavoriteMoviesDatabase::class.java,
"favorites.db"
).build().also { INSTANCE = it }
}
}
}
}
Perceba que para instanciar uma Database com databaseBuilder
, precisamos de uma variável chamada context: Context
.
No desenvolvimento Android, a variável context representa uma referência ao ambiente atual do aplicativo. O context fornece acesso a recursos e funcionalidades fundamentais do sistema, como arquivos, bancos de dados, preferências, serviços do sistema e componentes da interface. Ele é essencial para muitas operações, como exibir uma tela, acessar arquivos ou iniciar novas atividades. Existem diferentes tipos de contextos, como o ApplicationContext
(referente ao ciclo de vida do app) e o ActivityContext
(relacionado à atividade em execução).
No aplicativo exemplo, temos uma pequena alteração para que esse Contexto esteja disponível em qualquer arquivo do código. Essa alteração envolve os arquivos MainActivity.kt
, Application.kt
e AppContextHolder.kt
, que serão abordados mais a frente.
CastConverter.kt
Ainda na pasta data/local
temos um arquivo utilitário, responsável por realizar uma simples conversão de dados. Essa conversão é necessária pois, um dos campos de Movie
é a lista cast: List<String>
. Assim, convertemos essa lista para uma String que poderá ser serializada no momento de recuperação desse dado:
class CastConverter {
@TypeConverter
fun fromList(list: List<String>): String = list.joinToString(separator = ";")
@TypeConverter
fun toList(str: String): List<String> =
if (str.isEmpty()) emptyList() else str.split(";")
}
CastConverter.kt
package com.example.moviesapp.data.local
import androidx.room.TypeConverter
class CastConverter {
@TypeConverter
fun fromList(list: List<String>): String = list.joinToString(separator = ";")
@TypeConverter
fun toList(str: String): List<String> =
if (str.isEmpty()) emptyList() else str.split(";")
}
Data - Arquivos de repositories e providers
O restante da pasta data/
define nossos repositories e providers.
Um repository é uma camada de abstração responsável por gerenciar o acesso a diferentes fontes de dados, como banco de dados local (Room), serviços de rede (API), ou arquivos. Ele centraliza a lógica de obtenção, armazenamento e atualização de dados, oferecendo uma interface única para as demais partes do app (por exemplo, ViewModels). O uso do repository facilita a manutenção, os testes e a reutilização do código.
Um provider, por sua vez, é uma classe responsável por criar e/ou fornecer uma única instância de um objeto, geralmente utilizando o padrão Singleton. Por exemplo, um provider pode ser usado para produzir uma instância única do repository ao longo de toda a aplicação, garantindo que todos os componentes utilizem o mesmo objeto compartilhado. Essa abordagem facilita o gerenciamento do ciclo de vida das dependências e promove a reutilização e consistência no acesso aos recursos.
No caso da aplicação exemplo, temos repositories e providers para acesso remoto (API) e acesso local (Room).
Repository local
class LocalFavoriteMoviesRepository(private val dao: FavoriteMovieDao) {
suspend fun getAllFavorites(): List<FavoriteMovieEntity> = dao.getAll()
suspend fun getFavorite(movieId: Int): FavoriteMovieEntity? = dao.getById(movieId)
suspend fun isFavorite(movieId: Int): Boolean = dao.getById(movieId) != null
suspend fun addFavorite(movie: FavoriteMovieEntity) = dao.insert(movie)
suspend fun removeFavorite(movie: FavoriteMovieEntity) = dao.delete(movie)
}
LocalFavoriteMoviesRepository.kt
package com.example.moviesapp.data
import com.example.moviesapp.data.local.FavoriteMovieEntity
import com.example.moviesapp.data.local.FavoriteMovieDao
class LocalFavoriteMoviesRepository(private val dao: FavoriteMovieDao) {
suspend fun getAllFavorites(): List<FavoriteMovieEntity> = dao.getAll()
suspend fun getFavorite(movieId: Int): FavoriteMovieEntity? = dao.getById(movieId)
suspend fun isFavorite(movieId: Int): Boolean = dao.getById(movieId) != null
suspend fun addFavorite(movie: FavoriteMovieEntity) = dao.insert(movie)
suspend fun removeFavorite(movie: FavoriteMovieEntity) = dao.delete(movie)
}
Provider local
object LocalFavoriteMoviesRepositoryProvider {
@Volatile private var instance: LocalFavoriteMoviesRepository? = null
fun getRepository(context: Context): LocalFavoriteMoviesRepository {
return instance ?: synchronized(this) {
instance ?: LocalFavoriteMoviesRepository(
FavoriteMoviesDatabase.getInstance(context).favoriteMovieDao()
).also { instance = it }
}
}
}
LocalFavoriteMoviesRepositoryProvider.kt
package com.example.moviesapp.data
import android.content.Context
import com.example.moviesapp.data.local.FavoriteMoviesDatabase
object LocalFavoriteMoviesRepositoryProvider {
@Volatile private var instance: LocalFavoriteMoviesRepository? = null
fun getRepository(context: Context): LocalFavoriteMoviesRepository {
return instance ?: synchronized(this) {
instance ?: LocalFavoriteMoviesRepository(
FavoriteMoviesDatabase.getInstance(context).favoriteMovieDao()
).also { instance = it }
}
}
}
Repository remoto
class RemoteMoviesRepository(
private val apiService: MoviesApiService
) {
suspend fun getMovies(): List<Movie> {
return apiService.getMovies()
}
suspend fun getMovie(id: Int): Movie {
return apiService.getMovie(id)
}
suspend fun getReviews(movieId: Int): List<Review> {
return apiService.getReviews(movieId)
}
}
RemoteMoviesRepository.kt
package com.example.moviesapp.data
import com.example.moviesapp.network.MoviesApiService
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.Review
class RemoteMoviesRepository(
private val apiService: MoviesApiService
) {
suspend fun getMovies(): List<Movie> {
return apiService.getMovies()
}
suspend fun getMovie(id: Int): Movie {
return apiService.getMovie(id)
}
suspend fun getReviews(movieId: Int): List<Review> {
return apiService.getReviews(movieId)
}
}
Provider remoto
object RemoteMoviesRepositoryProvider {
val repository: RemoteMoviesRepository by lazy {
RemoteMoviesRepository(MoviesApi.retrofitService)
}
}
RemoteMoviesRepositoryProvider.kt
package com.example.moviesapp.data
import com.example.moviesapp.network.MoviesApi
object RemoteMoviesRepositoryProvider {
val repository: RemoteMoviesRepository by lazy {
RemoteMoviesRepository(MoviesApi.retrofitService)
}
}
Network
A pasta network/
armazena o arquivo para configuração do Retrofit
. Diferentemente da última aula, agora temos endpoints para acessar reviews:
interface MoviesApiService {
@GET("/movies")
suspend fun getMovies(): List<Movie>
@GET("/movies/{id}")
suspend fun getMovie(@Path("id") id:Int): Movie
@GET("/movie/{movieId}/reviews")
suspend fun getReviews(@Path("movieId") movieId:Int): List<Review>
}
object MoviesApi {
val retrofitService: MoviesApiService by lazy {
retrofit.create(MoviesApiService::class.java)
}
}
MoviesApiService.kt
package com.example.moviesapp.network
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.Review
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
private const val BASE_URL =
"https://moviesapi.kutzke.com.br"
private val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface MoviesApiService {
@GET("/movies")
suspend fun getMovies(): List<Movie>
@GET("/movies/{id}")
suspend fun getMovie(@Path("id") id:Int): Movie
@GET("/movie/{movieId}/reviews")
suspend fun getReviews(@Path("movieId") movieId:Int): List<Review>
}
object MoviesApi {
val retrofitService: MoviesApiService by lazy {
retrofit.create(MoviesApiService::class.java)
}
}
Utils
O arquivo utils/MapperExtensions.kt
é opcional. Ele realizar conversões entre dados locais e modelos da aplicação. Mas esse tipo de conversão não precisa ser utilizado. Por exemplo, o DAO poderia retornar diretamente um tipo Movie.
Porém, para que a abstração fique ainda mais evidente, fizemos uso dessa conversão.
fun Movie.toFavoriteEntity() = FavoriteMovieEntity(
id, title, cast, director, synopsis, posterUrl
)
fun FavoriteMovieEntity.toMovie() = Movie(
id, title, cast, director, synopsis, posterUrl
)
MapperExtensions.kt
package com.example.moviesapp.utils
import com.example.moviesapp.model.Movie
import com.example.moviesapp.data.local.FavoriteMovieEntity
fun Movie.toFavoriteEntity() = FavoriteMovieEntity(
id, title, cast, director, synopsis, posterUrl
)
fun FavoriteMovieEntity.toMovie() = Movie(
id, title, cast, director, synopsis, posterUrl
)
Ui
A partir daqui, os arquivos seguem, mais ou menos, o padrão da última aula. As telas foram, apenas, incrementadas.
Talvez a maior diferença seja que, agora, temos um ViewModel
para cada tela. Essa é uma prática comum e permite que os composables
de navegação
permaneçam simples, cuidando apenas da própria navegação.
Outra diferença é que, nessa versão, a navegação envolve uma topBar e uma bottomBar, definidas no Scaffold
da tela. Essa é uma composição
comum, garantido a aplicação uma navegabilidade conhecida nos dispositivos Android.
MoviesApp.kt
package com.example.moviesapp.ui
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.List
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.TopAppBar
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.fourMovies
import com.example.moviesapp.ui.moviesapp.MovieDetailsScreen
import com.example.moviesapp.ui.moviesapp.MoviesScreen
import com.example.moviesapp.ui.theme.MoviesAppTheme
sealed class BottomNavScreen(val route: String, val label: String, val icon: ImageVector) {
object MovieList : BottomNavScreen("movie_list", "Filmes", Icons.Filled.List)
object Favorites : BottomNavScreen("favorites", "Favoritos", Icons.Filled.Favorite)
companion object { val values = listOf(MovieList, Favorites) }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MoviesApp() {
val navController = rememberNavController()
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
val bottomNavRoutes = BottomNavScreen.values.map { it.route }
Scaffold(
topBar = {
val topBarTitle = when {
currentRoute?.startsWith("movie") == true -> "Filmes"
currentRoute?.startsWith("favorites") == true -> "Favoritos"
else -> ""
}
val showBack = currentRoute != null && bottomNavRoutes.none { currentRoute == it }
TopAppBar(
title = { Text(text = topBarTitle) },
navigationIcon = {
if(showBack) {
IconButton(
onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Localized description"
)
}
}
else null
},
)
},
bottomBar = {
NavigationBar {
BottomNavScreen.values.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = screen.label) },
label = { Text(screen.label) },
selected = (currentRoute == screen.route),
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = BottomNavScreen.MovieList.route,
modifier = Modifier.padding(innerPadding)
) {
composable(BottomNavScreen.MovieList.route) {
MoviesScreen(
onGoToMovieDetailsClick = { movieId ->
navController.navigate("movie_list/$movieId")
}
)
}
composable(BottomNavScreen.Favorites.route) {
FavoriteMoviesScreen()
}
composable(
route="movie_list/{movieId}",
arguments = listOf(
navArgument ("movieId") {
defaultValue = 0
type = NavType.IntType
}
)
) { backStackEntry ->
val movieId:Int? = backStackEntry.arguments?.getInt("movieId")
movieId?.let {
MovieDetailsScreen(
movieId = it,
onGoBackClick = {
navController.popBackStack()
}
)
}
}
}
}
}
@Composable
fun FavoriteMoviesScreen(
movies: List<Movie> = fourMovies,
) {
Column {
Text("Filmes Favoritos", style = MaterialTheme.typography.displayMedium)
if (movies.isEmpty()) Text("Nenhum favorito.")
movies.forEach { movie -> Text(movie.title) }
}
}
@Preview
@Composable
fun MoviesAppPreview(){
MoviesAppTheme{
MoviesApp()
}
}
ui/moviesapp
As telas MoviesScreen
e MovieDetailsScreen
foram remodeladas:
Para além das alterações visuais, que estão fora do escopo dessa aula (embora sejam bastante interessantes), as modificações mais importantes são as realizadas nos ViewModels
de cada tela.
MoviesListViewModel
Logo no inicio da definição do ViewModel
, observamos as inicializações das variáveis com os repositórios utilizados na tela.
As funções getMovies
e toggleFavorite
fazem o acesso aos repositórios local e remoto.
A função getMovies
solicita a lista de filmes através da API (repositório remoto) e a lista de filmes favoritos do usuário, a partir do Room (repositório local).
A função toggleFavorite
, por sua vez, realizar a verificação se o filme já está na tabela de filmes favoritos e na sequência, muda essa informação.
Com esses dados, a tela pode desenhar a lista de filmes, informando quais deles são favoritos.
class MoviesListViewModel(): ViewModel() {
private val repository = RemoteMoviesRepositoryProvider.repository
private val localRepository = LocalFavoriteMoviesRepositoryProvider.getRepository(AppContextHolder.appContext)
var moviesScreenUiState: MoviesScreenUiState by mutableStateOf(MoviesScreenUiState.Loading)
init {
getMovies()
}
private fun getMovies(){
viewModelScope.launch {
moviesScreenUiState = try {
val movies = repository.getMovies()
val favorites = localRepository.getAllFavorites().map { it.toMovie() }
MoviesScreenUiState.Success(movies = movies, favorites = favorites)
}
catch(e: IOException){
MoviesScreenUiState.Error
}
}
}
fun toggleFavorite(movieId:Int){
if(moviesScreenUiState is MoviesScreenUiState.Success){
val movieToToggle: Movie? = (moviesScreenUiState as MoviesScreenUiState.Success).movies.find{ movie -> movieId == movie.id}
movieToToggle?.let{
viewModelScope.launch {
if(localRepository.getFavorite(movieId) == null){
localRepository.addFavorite(movieToToggle.toFavoriteEntity())
}
else{
localRepository.removeFavorite(movieToToggle.toFavoriteEntity())
}
val favorites = localRepository.getAllFavorites().map { it.toMovie() }
moviesScreenUiState = MoviesScreenUiState.Success(movies = (moviesScreenUiState as MoviesScreenUiState.Success).movies, favorites = favorites)
}
}
}
}
}
MoviesListViewModel.kt
package com.example.moviesapp.ui.moviesapp
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.moviesapp.AppContextHolder
import com.example.moviesapp.data.LocalFavoriteMoviesRepository
import com.example.moviesapp.data.LocalFavoriteMoviesRepositoryProvider
import com.example.moviesapp.data.RemoteMoviesRepositoryProvider
import com.example.moviesapp.model.Movie
import com.example.moviesapp.utils.toFavoriteEntity
import com.example.moviesapp.utils.toMovie
import kotlinx.coroutines.launch
import okio.IOException
sealed interface MoviesScreenUiState {
class Success(val movies: List<Movie>, val favorites: List<Movie>): MoviesScreenUiState
object Error: MoviesScreenUiState
object Loading: MoviesScreenUiState
}
class MoviesListViewModel(): ViewModel() {
private val repository = RemoteMoviesRepositoryProvider.repository
private val localRepository = LocalFavoriteMoviesRepositoryProvider.getRepository(AppContextHolder.appContext)
var moviesScreenUiState: MoviesScreenUiState by mutableStateOf(MoviesScreenUiState.Loading)
init {
getMovies()
}
private fun getMovies(){
viewModelScope.launch {
moviesScreenUiState = try {
val movies = repository.getMovies()
val favorites = localRepository.getAllFavorites().map { it.toMovie() }
MoviesScreenUiState.Success(movies = movies, favorites = favorites)
}
catch(e: IOException){
MoviesScreenUiState.Error
}
}
}
fun toggleFavorite(movieId:Int){
if(moviesScreenUiState is MoviesScreenUiState.Success){
val movieToToggle: Movie? = (moviesScreenUiState as MoviesScreenUiState.Success).movies.find{ movie -> movieId == movie.id}
movieToToggle?.let{
viewModelScope.launch {
if(localRepository.getFavorite(movieId) == null){
localRepository.addFavorite(movieToToggle.toFavoriteEntity())
}
else{
localRepository.removeFavorite(movieToToggle.toFavoriteEntity())
}
val favorites = localRepository.getAllFavorites().map { it.toMovie() }
moviesScreenUiState = MoviesScreenUiState.Success(movies = (moviesScreenUiState as MoviesScreenUiState.Success).movies, favorites = favorites)
}
}
}
}
}
MovieDetailsViewModel
A tela de detalhes de um filme, também precisa acessar ambos os repositórios. Se o filme for favorito, os dados são obtidos localmente, caso contrário, tenta-se acessar a API pelo repositórios remoto.
Os reviews são sempre acessados pela API.
Isso permite um comportamento interessante da tela caso o usuário não possua acesso a internet.
class MovieDetailsViewModel(): ViewModel() {
private val remoteRepository = RemoteMoviesRepositoryProvider.repository
private val localRepository = LocalFavoriteMoviesRepositoryProvider.getRepository(AppContextHolder.appContext)
var movieDetailsUiState: MovieDetailsUiState by mutableStateOf(MovieDetailsUiState.Loading)
fun getMovie(movieId:Int){
var movie: Movie?
var reviews: List<Review>
movie = null
viewModelScope.launch {
movieDetailsUiState = try {
movie = localRepository.getFavorite(movieId)?.toMovie() ?:
remoteRepository.getMovie(movieId)
reviews = remoteRepository.getReviews(movieId)
MovieDetailsUiState.Success(movie = movie, reviews= reviews)
}
catch(e: IOException) {
try {
movie = localRepository.getFavorite(movieId)?.toMovie()
?: remoteRepository.getMovie(movieId)
MovieDetailsUiState.SuccessButNoReviews(movie = movie, reviews = null)
} catch (e: IOException) {
MovieDetailsUiState.Error
}
}
}
}
}
MovieDetailsViewModel.kt
package com.example.moviesapp.ui.moviesapp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.moviesapp.AppContextHolder
import com.example.moviesapp.data.LocalFavoriteMoviesRepositoryProvider
import com.example.moviesapp.data.RemoteMoviesRepositoryProvider
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.Review
import com.example.moviesapp.utils.toMovie
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import okio.IOException
sealed interface MovieDetailsUiState {
class Success(val movie: Movie?, val reviews: List<Review>): MovieDetailsUiState
class SuccessButNoReviews(val movie: Movie?, val reviews: List<Review>?): MovieDetailsUiState
object Error: MovieDetailsUiState
object Loading: MovieDetailsUiState
}
class MovieDetailsViewModel(): ViewModel() {
private val remoteRepository = RemoteMoviesRepositoryProvider.repository
private val localRepository = LocalFavoriteMoviesRepositoryProvider.getRepository(AppContextHolder.appContext)
var movieDetailsUiState: MovieDetailsUiState by mutableStateOf(MovieDetailsUiState.Loading)
fun getMovie(movieId:Int){
var movie: Movie?
var reviews: List<Review>
movie = null
viewModelScope.launch {
movieDetailsUiState = try {
movie = localRepository.getFavorite(movieId)?.toMovie() ?:
remoteRepository.getMovie(movieId)
reviews = remoteRepository.getReviews(movieId)
MovieDetailsUiState.Success(movie = movie, reviews= reviews)
}
catch(e: IOException) {
try {
movie = localRepository.getFavorite(movieId)?.toMovie()
?: remoteRepository.getMovie(movieId)
MovieDetailsUiState.SuccessButNoReviews(movie = movie, reviews = null)
} catch (e: IOException) {
MovieDetailsUiState.Error
}
}
}
}
}
Demais arquivos de interface
Vale mencionar que a tela MovieDetailsScreen
faz uso da função rememberLazyListState
para implementar o efeito de rolagem conhecido como Collapsing Toolbar, fazendo com que a imagem diminua de tamanha a medida que o usuário rola a tela.
O código abaixo faz o calcula da altura atual que a foto deve ter.
scrollState
terá várias informações sobre o estado da rolagem da LazyColumn
.
important
A implementação abaixo não é ideal. Ela favorece a simplicidade em detrimento do desempenho. O código apresentado realiza muitas recomposições, o que pode impactar o desempenho da aplicação.
val scrollState = rememberLazyListState()
val density = LocalDensity.current
val maxHeight = 500.dp
val minHeight = 150.dp
val maxHeightPx = with(density) { maxHeight.toPx() }
val minHeightPx = with(density) { minHeight.toPx() }
val scroll: Float =
(scrollState.firstVisibleItemIndex * maxHeightPx + scrollState.firstVisibleItemScrollOffset)
val currentHeightPx = (maxHeightPx - scroll).coerceIn(minHeightPx, maxHeightPx)
val currentHeight = with(density) { currentHeightPx.toDp() }
MovieDetailsScreen.kt
package com.example.moviesapp.ui.moviesapp
import android.R
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.ColorImage
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.Review
import com.example.moviesapp.model.fourMovies
import com.example.moviesapp.model.fourReviews
import com.example.moviesapp.ui.theme.MoviesAppTheme
@Composable
fun MovieDetailsScreen(
movieId: Int,
movieDetailsViewModel: MovieDetailsViewModel = viewModel(),
onGoBackClick: () -> Unit = {},
){
val movieDetailsUiState = movieDetailsViewModel.movieDetailsUiState
LaunchedEffect(movieId) {
movieDetailsViewModel.getMovie(movieId)
}
when(movieDetailsUiState){
is MovieDetailsUiState.Success ->{
MovieDetailsScreen(movie = movieDetailsUiState.movie!!, reviews = movieDetailsUiState.reviews)
}
is MovieDetailsUiState.SuccessButNoReviews ->{
MovieDetailsScreen(movie = movieDetailsUiState.movie!!, reviews = null)
}
is MovieDetailsUiState.Loading ->{
LoadingScreen(modifier = Modifier.fillMaxSize())
}
is MovieDetailsUiState.Error ->{
ErrorScreen(modifier = Modifier.fillMaxSize())
}
}
}
@OptIn(ExperimentalCoilApi::class)
@Composable
fun MovieDetailsScreen(
movie: Movie = fourMovies[0],
reviews: List<Review>? = fourReviews,
onGoBackClick: () -> Unit = {},
){
val scrollState = rememberLazyListState()
val density = LocalDensity.current
val maxHeight = 500.dp
val minHeight = 150.dp
val maxHeightPx = with(density) { maxHeight.toPx() }
val minHeightPx = with(density) { minHeight.toPx() }
val scroll: Float =
(scrollState.firstVisibleItemIndex * maxHeightPx + scrollState.firstVisibleItemScrollOffset)
val currentHeightPx = (maxHeightPx - scroll).coerceIn(minHeightPx, maxHeightPx)
val currentHeight = with(density) { currentHeightPx.toDp() }
Box {
LazyColumn(
state = scrollState,
contentPadding = PaddingValues(top = maxHeight),
) {
item() {
Column(modifier = Modifier
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.size(20.dp))
Text(text="Sinopse", style = MaterialTheme.typography.titleLarge)
Text(text = movie.synopsis)
Spacer(modifier = Modifier.size(20.dp))
Text(text="Diretor", style = MaterialTheme.typography.titleLarge)
Text(text = movie.director)
Spacer(modifier = Modifier.size(20.dp))
Text(text="Elenco", style = MaterialTheme.typography.titleLarge)
Text(text = movie.cast.toString())
HorizontalDivider(modifier = Modifier.padding(horizontal = 10.dp, vertical = 25.dp))
Text(text = "Reviews", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(25.dp))
}
}
item() {
Column(modifier = Modifier
.padding(horizontal = 20.dp)
) {
if(reviews == null){
ErrorScreen(modifier = Modifier.fillMaxSize())
}
else if(reviews.isEmpty()){
Text(text= "Nenhum review :(")
}
else {
reviews.forEach { review ->
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = review.author, modifier = Modifier.weight(1f), style = MaterialTheme.typography.titleMedium)
Text(text = "${review.rating} / 10")
}
Text(text = review.reviewText)
Spacer(modifier = Modifier.height(25.dp))
}
}
}
}
}
Box {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) {
AsyncImage(
model = movie.posterUrl,
contentDescription = "Poster de ${movie.title}",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(currentHeight)
.align(Alignment.TopCenter)
)
}
Text(
text = movie.title,
style = MaterialTheme.typography.displaySmall.copy(
shadow = Shadow(
color = Color.Black, offset = Offset(x = 0f,y = 0f), blurRadius = 10f
)
),
color = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.padding(10.dp)
.align(Alignment.BottomStart)
)
}
}
}
val previewHandler = AsyncImagePreviewHandler {
ColorImage(Color.Red.toArgb())
}
@Preview
@Composable
fun MovieDetailsScreenPreview() {
MoviesAppTheme {
MovieDetailsScreen(
movie = fourMovies[0]
)
}
}
MoviesScreen.kt
package com.example.moviesapp.ui.moviesapp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.moviesapp.model.fourMovies
import com.example.moviesapp.ui.theme.MoviesAppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MoviesScreen(
moviesListViewModel: MoviesListViewModel = viewModel (),
onGoToMovieDetailsClick: (movieId:Int) -> Unit = {},
) {
val moviesScreenUiState = moviesListViewModel.moviesScreenUiState
Column(){
when(moviesScreenUiState){
is MoviesScreenUiState.Success -> {
MoviesList(
movies = moviesScreenUiState.movies,
favorites = moviesScreenUiState.favorites,
onFavoriteClick = { movieId -> moviesListViewModel.toggleFavorite(movieId) },
onMovieClick = {movieId -> onGoToMovieDetailsClick(movieId) },
)
}
is MoviesScreenUiState.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
is MoviesScreenUiState.Error -> ErrorScreen( modifier = Modifier.fillMaxSize())
}
}
}
MoviesList.kt
MovieItem.kt
package com.example.moviesapp.ui.moviesapp
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.ColorImage
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import com.example.moviesapp.model.Movie
import com.example.moviesapp.model.fourMovies
import com.example.moviesapp.ui.theme.MoviesAppTheme
@OptIn(ExperimentalCoilApi::class)
@Composable
fun MovieItem(
modifier: Modifier = Modifier,
movie: Movie,
isFavorite: Boolean = false,
onTitleClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}
) {
val previewHandler = AsyncImagePreviewHandler{
ColorImage(Color.Red.toArgb())
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp)
) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides com.example.moviesapp.ui.moviesapp.previewHandler) {
AsyncImage(
model = movie.posterUrl,
contentDescription = "Poster de ${movie.title}",
contentScale = ContentScale.Crop,
modifier = Modifier
.height(50.dp)
.width(50.dp)
.align(Alignment.CenterVertically)
.padding(end = 10.dp)
)
}
Text(
modifier = modifier
.clickable(
onClick = onTitleClick,
)
.weight(1f),
text = movie.title,
)
IconButton(
onClick = onFavoriteClick,
) {
if(isFavorite) {
Icon(Icons.Filled.Favorite, contentDescription = "Toogle Favorite")
}
else{
Icon(Icons.Filled.FavoriteBorder, contentDescription = "Toogle Favorite")
}
}
}
}
@Preview
@Composable
fun MovieItemPreview(){
MoviesAppTheme {
MovieItem(movie = fourMovies[0])
}
}
ApiComposables.kt
package com.example.moviesapp.ui.moviesapp
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.example.moviesapp.R
import androidx.compose.ui.res.stringResource
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
Image(
modifier = modifier.size(200.dp),
painter = painterResource(R.drawable.loading_img),
contentDescription = "Carregando"
)
}
@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
)
Text(text = "Falha ao Carregar", modifier = Modifier.padding(16.dp))
}
}
App
Como informado anteriormente, os arquivos abaixo tem pequenas alterações para permitir uma instância de context: Context
disponível em todo o código da aplicação. O context
é necessário para que os viewModels
inicializem os repositórios.
Application.kt
package com.example.moviesapp
import android.app.Application
class MoviesApplication : Application() {
override fun onCreate() {
super.onCreate()
AppContextHolder.init(this)
}
}
AppContextHolder.kt
package com.example.moviesapp
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
object AppContextHolder {
@SuppressLint("StaticFieldLeak")
lateinit var appContext: Context
private set
fun init(app: Application) {
appContext = app.applicationContext
}
}
Para que tudo isso funcione, precisamos informar no arquivo AndroidManifest.xml
que o entry point da aplicação agora deve ser a classe MoviesApplication
e não mais a MainActivity
. Para isso, alteramos o atributo name
do item Application
.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MoviesApplication"
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MoviesApplication"
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MoviesApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MoviesApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
10 - Arquitetura de Aplicações Android e Outros Conceitos Importantes
Considerações Iniciais
- Limitações de Recursos: Dispositivos móveis podem encerrar processos de aplicativos para liberar recursos.
- Arquitetura de Aplicativos: Define os limites e responsabilidades de cada parte do aplicativo.
Princípios Fundamentais
Separação de Responsabilidades
- Evitar concentrar todo o código em uma Activity ou Fragment.
Interface Guiada por Modelos de Dados
- A UI deve ser baseada em modelos de dados, preferencialmente persistentes.
- Benefícios: Aplicativos mais testáveis e robustos.
Fonte Única de Verdade
- Designar uma única fonte de verdade (SSOT) para cada tipo de dado.
- Centraliza mudanças, protege dados contra alterações indevidas e facilita o rastreamento de bugs.
Fluxo de Dados Unidirecional
- Utiliza-se o padrão de Fluxo de Dados Unidirecional (UDF) onde o estado flui em uma direção e eventos na direção oposta.
Arquitetura Recomendada
Camadas do Aplicativo
- Camada de UI: Exibe os dados na tela.
- Camada de Dados: Contém a lógica de negócios e expõe os dados.
Técnicas Modernas
- Arquitetura reativa e em camadas.
- Fluxo de Dados Unidirecional (UDF).
- Uso de Coroutines e Flows.
- Injeção de dependência.
Camadas Detalhadas
Camada de UI
- Elementos de UI: Renderizam os dados.
- Portadores de Estado: Mantêm e gerenciam os dados do UI (exemplo: ViewModel).
Camada de Dados
- Repositórios: Expõem e centralizam dados, resolvem conflitos entre fontes e contêm lógica de negócios.
Camada de Domínio (Opcional)
- Função: Encapsula lógica de negócios complexa que pode, por exemplo, ser reutilizada por vários ViewModels.
Injeção de Dependência
- Injeção de Dependência (DI): Permite definir dependências sem construi-las diretamente. Durante a execução, outra classe é responsável por prover essas dependências.
- Service Locator: Padrão que fornece um registro para obter dependências. Ou seja, durante a execução, classes podem consultar o registro para obter suas dependências.
A biblioteca Hilt é indicada para o uso de DI em projetos Android:
Melhores Práticas Gerais
- Não armazene dados em componentes de aplicativos.
- Reduza dependências em classes Android (seus componentes devem ser os únicos a dependerem de Apis do Android, como o
Context
). - Crie limites bem definidos entre módulos.
- Minimize a exposição de cada módulo.
- Testabilidade e segurança de tipos.
Benefícios da Arquitetura
- Manutenção e Qualidade: Melhora a robustez geral do aplicativo.
- Escalabilidade: Facilita a contribuição de múltiplas pessoas e equipes.
- Onboarding: Consistência simplifica a adaptação de novos integrantes.
- Testabilidade: Arquitetura favorece tipos simples e testáveis.
Recomendações para Arquitetura de Aplicações Android
https://developer.android.com/topic/architecture/recommendations
Outros conteúdos
Distribuição de Aplicativos
- APKs Otimizados: No Google Play, servidores geram APKs otimizados contendo apenas recursos necessários para o dispositivo que solicita a instalação.
Sistema Operacional Android
- Sistema Multiusuário: Android é um sistema multiusuário baseado em Linux; cada app é um usuário diferente.
- Permissões de Arquivos: Cada app tem permissões para acessar apenas seus próprios arquivos.
- Processo Isolado: Cada app executa em seu próprio processo Linux e máquina virtual, garantindo isolamento.
- Princípio do Menor Privilégio: Apps acessam apenas componentes necessários para sua execução.
Permissões
- Solicitação de Permissões: Apps podem solicitar acesso a dados do dispositivo como localização e câmera, mas o usuário deve conceder explicitamente essas permissões. Mais informações sobre permissões.
Tipos de Componentes de Aplicativos
-
Activities
- Ponto de entrada para interação com o usuário, representando uma tela.
- Diferentes apps podem iniciar activities se permitido.
-
Services
- Executa operações em segundo plano sem interface de usuário.
- Ex.: Tocar música, buscar dados na rede.
-
Broadcast Receivers
- Permite receber eventos do sistema e reagir a eles.
- Ex.: Notificação de eventos futuros.
-
Content Providers
- Gerencia dados entre aplicativos.
- Por exemplo o sistema Android expõe um Content Provider que gerencia informações de contato do usuário. Qualquer Aplicação com permissões suficientes pode gerenciar essas informações;
- Acesso permitido a apps com as permissões corretas.
Interação entre Aplicativos
- Início de Componentes de Outros Apps: Apps podem iniciar componentes de outros apps sem incorporar código de terceiros.
- Intents: Mensagens assíncronas que ativam três dos quatro tipos de componentes (activities, services, receivers):
- Por exemplo, uma aplicação pode enviar uma
Intent
para o aplicativo nativo de câmera para capturar uma foto. - Nesse caso, não é necessário implementar um acesso à câmera. Basta utilizar a tela do aplicativo nativo.
- Esse relacionamento pode ser obtido com qualquer aplicativo que exponha
Intents
para acesso aos seus recursos.
- Por exemplo, uma aplicação pode enviar uma
- Manifesto do Aplicativo: Arquivo
AndroidManifest.xml
declara componentes e permissões de uso.
Exemplo de Intent
para compartilhar um filme na Aplicação Movies App.
Declaração de Componentes
-
Elementos no Manifesto:
<activity>
para activities<service>
para services<receiver>
para broadcast receivers<provider>
para content providers
-
Declaração de Câmera:
- Apps declaram recursos de hardware, como câmera, no arquivo
AndroidManifest.xml
. - Permite especificar se o recurso é necessário ou opcional.
- Apps declaram recursos de hardware, como câmera, no arquivo
Especificação Trabalho Prático
Tema: Desenvolvimento de uma Aplicação Android para Gestão de Tarefas
Objetivo: Criar uma aplicação Android que permita aos usuários gerenciar suas tarefas diárias, integrando conhecimentos de arquitetura e testes de aplicações Android.
Descrição do Projeto: Os alunos devem desenvolver uma aplicação Android com as seguintes funcionalidades e requisitos:
Este trabalho poderá ser realizado em grupos de até 4 estudantes.
Funcionalidades Básicas
-
Autenticação:
- Autenticação com Firebase Authentication;
-
Persistência de dados:
- Persistência de dados com Firebase Firestore ou Room Database;
-
Modelo de dados:
- Tarefas devem ter, ao menos, as seguintes informações:
- Título;
- Descrição;
- Data limite;
- Prioridade;
- Status;
- Tarefas podem ter subtarefas;
- Uma tarefa pode ser iniciada, pausada e concluída;
- Uma tarefa pode ser concluída diretamente, sem precisar sem iniciada;
- Tarefas devem ter, ao menos, as seguintes informações:
-
Funcionalidades gerais:
- Permitir criação, edição e remoção de tarefas;
- Notificar usuários sobre tarefas com data limite próxima;
- Apresentar tempo utilizado para concluir uma tarefa;
-
Requisito de interface:
- Utilizar Jetpack Compose para criação das telas.
- O layout da aplicação e a quantidade de telas é livre.
-
Filtro e Ordenação:
- Permitir filtragem por prioridade e status.
- Ordenação por data de conclusão.
Funcionalidades Adicionais (desejáveis, porém, opcionais)
-
Tema Escuro/Claro:
- Configurar alternância entre temas escuro e claro.
-
Pesquisa de Tarefas:
- Implementar barra de pesquisa para encontrar tarefas rapidamente.
Sugestões para Telas da Aplicação
-
Tela de Autenticação:
- Login e Registro: Usar Firebase Authentication.
-
Tela de Dashboard:
- Resumo de Tarefas: Exibir contagem de tarefas pendentes, concluídas e totais.
- Listagem de Tarefas: Mostrar as tarefas em formato de lista.
-
Tela de Detalhes da Tarefa:
- Visualizar Detalhes: Título, descrição, data, prioridade e status.
- Botões de Ação: Editar ou excluir a tarefa.
-
Tela de Criação/Edição de Tarefa:
- Entrada de Dados: Título, descrição, data de conclusão, prioridade.
- Salvar/Atualizar Tarefa: Persistir dados no Firebase Firestore ou Room Database.
-
Tela de Notificações:
- Gerenciar Notificações: Ativar/desativar lembretes para tarefas específicas.
-
Tela de Perfil do Usuário:
- Editar Perfil: Nome do usuário, e-mail e outros dados relevantes.
- Sair da Conta: Logout da aplicação.
Estrutura da Aplicação
- Arquitetura: Utilizar MVVM para uma separação clara de responsabilidades.
- Banco de Dados: Firebase Firestore ou Room Database para persistência.
Entregáveis
-
Código-Fonte:
- Repositório do GitLab com código bem documentado.
-
Documentação Técnica: (no próprio repositório)
- Instruções para instalação, execução e detalhes sobre arquitetura utilizada.
-
Vídeo de funcionamento:
- Video apresentando a aplicação em funcionamento.
- Este vídeo ficará disponível para que todos da turma conheçam os aplicativos de outros grupos.
Defesa do trabalho
- Apresentação final destacando funcionalidades e processo de desenvolvimento.
- Apenas para o professor.
- Aplicação deve funcionar em um dispositivo físico.
Critérios de Avaliação
A avaliação será em duas partes.
-
Avaliação final do projeto e funcionalidade (70% da nota):
- Funcionalidade Completa: Implementação de todos os requisitos.
- Qualidade do Código: Uso correto da arquitetura MVVM e boas práticas.
- Interface do Usuário: Design coerente com Material Design.
- Documentação e código fonte: Clareza e cobertura ampla das informações técnicas e bom gerenciamento do versionamento do código.
-
Avaliação do processo de desenvolvimento (30% da nota):
- 4x checkpoints nas aulas dos dias 21/05, 28/05, 04/06 e 11/06.
Os checkpoints poderão ser realizados de duas formas:
- Commits regulares antes de cada data de checkpoint (professor irá avaliar commits realizados no repositório).
- Reuniões presenciais no horário da aula;
Cada equipe poderá decidir, semanalmente, a forma de checkpoint que deseja.
Repositório para o projeto
O repositório base para o projeto final é o seguinte:
https://gitlab.com/ds151-alexkutzke/ds151-project-2025-1
Cada equipe deve fazer apenas 1 fork do projeto acima.
Os membros da equipe devem ser adicionados pela interface do Gitlab como desenvolvedores no projeto. O arquivo README deverá conter o nome dos integrantes do grupo.
O fork de cada equipe deverá ser realizado antes da primeira data de checkpoint.