📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Este é o seu projeto de Transição da Fase 2: A Ponte para o Moderno! 🌉 Você continuará utilizando o desenho de tela tradicional em arquivos XML, mas escreverá toda a lógica de programação na linguagem Kotlin. É aqui que você sentirá o poder de uma linguagem moderna com menos linhas de código, segurança a nulos e facilidades assíncronas!
Antes de começar, certifique-se de já ter estudado:
Criar o clássico console de brinquedo Genius (Simon Says). O jogo sorteia uma sequência aleatória de cores e sons. A cada rodada, a sequência aumenta. O jogador deve observar os flashes luminosos e repetir a sequência exata de botões clicando nas cores.
💡 O mesmo conceito em outra stack: o clássico Genius/Simon também é construído do zero em Java Swing no curso Sistemas com Java (com as evoluções
_02_swe_03_sw, refatorando para MVC).
graph TD
A[Início / App Pronto] --> B[Clique em INICIAR DESAFIO]
B --> C[Limpar dados & Prepare-se]
C --> D[Iniciar Nova Rodada]
D --> E[Incrementar Nível & Sorteia nova cor]
E --> F[Tocar Sequência de Cores Computador]
F --> G[Aguardar clique do Jogador]
G --> H[Clique na cor do Jogador]
H --> I{Cor clicada é igual à da sequência?}
I -- Sim --> J[Avançar índice do jogador]
I -- Não --> K[Game Over!🏆 Salvar High Score]
J --> L{Jogador completou toda a sequência?}
L -- Sim --> M[Aguardar 1s & Iniciar Nova Rodada]
L -- Não --> G
M --> D
K --> N[Exibir Botão Start com Tentar Novamente]
null-safety) os componentes do XML direto no Kotlin, aposentando de vez o antigo e perigoso findViewById.Activity). Garante que qualquer tarefa assíncrona ou animação seja encerrada automaticamente caso o app seja fechado, evitando vazamento de memória.High Score).Para que a imersão de fliperama clássico fique perfeita, baixe os efeitos de áudio originais do console Simon:
res..wav (blue.wav, green.wav, red.wav e yellow.wav) para dentro da pasta res/raw.New Project.Genius Simon Gamebr.com.curso.geniusAbra o arquivo de configuração do app: Gradle Scripts > build.gradle (Module :app) e adicione a configuração do viewBinding dentro do bloco android:
android {
...
buildFeatures {
viewBinding true
}
}
Após alterar, clique em Sync Now no topo direito da tela.
Faremos o console clássico desenhando quatro setores esféricos usando shapes do Android XML. Cada botão colorido terá cantos assimétricos e luz radial interna.
Vá em app > src > main > res > drawable e crie um arquivo chamado quadrant_green.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:startColor="#66BB6A"
android:endColor="#1B5E20"
android:centerX="0.3"
android:centerY="0.3" />
<corners
android:topLeftRadius="150dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="40dp"
android:bottomRightRadius="10dp" />
<stroke android:width="2dp" android:color="#00000033" />
</shape>
Crie os outros três arquivos na pasta drawable alterando apenas as cores e os cantos correspondentes para formar o círculo central (conforme os códigos completos disponíveis na seção Gabarito ao final do guia).
Desenharemos a mesa preta esférica do console combinando ConstraintLayout para o cabeçalho e FrameLayout empilhando a borda redonda externa e o núcleo de texto central.
Abra app > src > main > res > layout > activity_main.xml e cole o código de design completo fornecido no Gabarito.
A grande estrela da lógica deste app é o uso de suspend functions e o temporizador delay() de coroutines para piscar os botões sequencialmente sem bloquear a tela do celular.
Abra o arquivo de lógica em Kotlin: app > src > main > java > br > com > curso > genius > MainActivity.kt e codifique o fluxo com base no Gabarito Oficial.
Se o compilador reclamar que a classe ActivityMainBinding não existe, clique no menu superior Build > Clean Project e depois em Build > Rebuild Project para forçar o Android Studio a gerá-la.
Veja como sua tela deve ficar ao final deste projeto:

activity_main.xml)Disponível em: app/src/main/res/layout/activity_main.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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#050505"
android:padding="16dp">
<!-- Cabeçalho com Placar Moderno -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="20dp"
android:background="#1A1A1A"
android:padding="12dp">
<TextView
android:id="@+id/txt_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LEVEL: 0"
android:textColor="#00E676"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginEnd="40dp"/>
<TextView
android:id="@+id/txt_high_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BEST: 0"
android:textColor="#FFC107"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/txt_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OBSERVE A SEQUÊNCIA"
android:textColor="#888888"
android:textSize="16sp"
android:letterSpacing="0.1"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- CONSOLE SIMON PRO (Base com profundidade) -->
<FrameLayout
android:id="@+id/console_outer"
android:layout_width="360dp"
android:layout_height="360dp"
android:background="@drawable/center_circle"
android:backgroundTint="#000000"
android:padding="8dp"
app:layout_constraintBottom_toTopOf="@+id/btn_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txt_status">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/center_circle"
android:backgroundTint="#1A1A1A"
android:padding="15dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_verde"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@drawable/quadrant_green"
app:backgroundTint="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="3dp" />
<Button
android:id="@+id/btn_vermelho"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@drawable/quadrant_red"
app:backgroundTint="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="3dp" />
<Button
android:id="@+id/btn_amarelo"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@drawable/quadrant_yellow"
app:backgroundTint="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_margin="3dp" />
<Button
android:id="@+id/btn_azul"
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@drawable/quadrant_blue"
app:backgroundTint="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="3dp" />
<!-- HUB CENTRAL DETALHADO -->
<View
android:layout_width="140dp"
android:layout_height="140dp"
android:background="@drawable/center_circle"
android:backgroundTint="#000000"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Botão Fake de Power -->
<View
android:id="@+id/power_btn"
android:layout_width="30dp"
android:layout_height="15dp"
android:layout_marginBottom="45dp"
android:background="@android:drawable/editbox_dropdown_light_frame"
android:backgroundTint="#333333"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SIMON"
android:textColor="#FFFFFF"
android:textSize="28sp"
android:textStyle="bold"
android:textAllCaps="true"
android:letterSpacing="0.1"
android:fontFamily="sans-serif-black"
app:layout_constraintTop_toBottomOf="@+id/power_btn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="4dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</FrameLayout>
<Button
android:id="@+id/btn_start"
android:layout_width="match_parent"
android:layout_height="65dp"
android:layout_marginBottom="20dp"
android:backgroundTint="#00E676"
android:text="INICIAR DESAFIO"
android:textColor="#000000"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt)Disponível em: app/src/main/java/br/com/curso/genius/MainActivity.kt
package br.com.curso.genius
import android.content.Context
import android.media.MediaPlayer
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import br.com.curso.genius.databinding.ActivityMainBinding
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
// ViewBinding para acessar componentes sem findViewById
private lateinit var binding: ActivityMainBinding
// Arrays dinâmicos de jogo
private val sequenciaComputador = mutableListOf<Int>()
private var passoJogador = 0
private var score = 0
private var highScore = 0
private var jogoAtivo = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Inicializa o ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Carrega recorde salvo localmente
highScore = getPreferences(Context.MODE_PRIVATE).getInt("HIGH_SCORE", 0)
atualizarPlacar()
configurarBotoes()
binding.btnStart.setOnClickListener { iniciarJogo() }
}
/**
* Mapeia os listeners de clique dos 4 botões coloridos
*/
private fun configurarBotoes() {
val botoes = listOf(binding.btnVerde, binding.btnVermelho, binding.btnAmarelo, binding.btnAzul)
botoes.forEachIndexed { index, button ->
button.setOnClickListener {
if (jogoAtivo) aoClicar(index)
}
button.isEnabled = false // Travados até clicar em Start
}
}
/**
* Inicia a sequência de rodadas do jogo
*/
private fun iniciarJogo() {
sequenciaComputador.clear()
score = 0
binding.btnStart.visibility = View.GONE
binding.txtStatus.text = "PREPARE-SE!"
iniciarNovaRodada()
}
/**
* Incrementa o nível e adiciona um novo passo aleatório ao desafio
*/
private fun iniciarNovaRodada() {
passoJogador = 0
score = sequenciaComputador.size
atualizarPlacar()
// Sorteia número de 0 a 3
sequenciaComputador.add((0..3).random())
// Lança uma Coroutine no escopo de vida da tela
lifecycleScope.launch {
tocarSequencia()
}
}
/**
* Executa o piscar das cores sequencialmente com áudio correspondente
*/
private suspend fun tocarSequencia() {
jogoAtivo = false
binding.txtStatus.text = "OBSERVE..."
delay(1000)
val botoes = listOf(binding.btnVerde, binding.btnVermelho, binding.btnAmarelo, binding.btnAzul)
sequenciaComputador.forEach { corIndex ->
val botao = botoes[corIndex]
piscarBotao(botao, corIndex)
delay(800) // Intervalo entre piscadas
}
binding.txtStatus.text = "SUA VEZ!"
jogoAtivo = true
botoes.forEach { it.isEnabled = true }
}
/**
* Altera a transparência temporariamente simulando um piscar físico
*/
private suspend fun piscarBotao(botao: Button, indice: Int) {
tocarSom(indice)
botao.alpha = 0.3f
delay(400) // Tempo que o botão fica aceso
botao.alpha = 1.0f
}
/**
* Toca o áudio correspondente ao botão colorido
*/
private fun tocarSom(indice: Int) {
val somResId = when (indice) {
0 -> R.raw.green
1 -> R.raw.red
2 -> R.raw.yellow
3 -> R.raw.blue
else -> 0
}
if (somResId != 0) {
// MediaPlayer.create é prático, mas recriar toda vez consome memória.
// Para resolver isso, usamos o setOnCompletionListener para liberar o áudio do celular ao terminar.
val mp = MediaPlayer.create(this, somResId)
mp.setOnCompletionListener { it.release() }
mp.start()
}
}
/**
* Compara a escolha do usuário com o gabarito do computador
*/
private fun aoClicar(indice: Int) {
val botoes = listOf(binding.btnVerde, binding.btnVermelho, binding.btnAmarelo, binding.btnAzul)
tocarSom(indice)
lifecycleScope.launch {
botoes[indice].alpha = 0.3f
delay(100)
botoes[indice].alpha = 1.0f
}
if (indice == sequenciaComputador[passoJogador]) {
passoJogador++
if (passoJogador == sequenciaComputador.size) {
jogoAtivo = false
binding.txtStatus.text = "MUITO BEM!"
lifecycleScope.launch {
delay(1000)
iniciarNovaRodada()
}
}
} else {
gameOver()
}
}
/**
* Executa a regra ao errar a sequência
*/
private fun gameOver() {
jogoAtivo = false
binding.txtStatus.text = "GAME OVER!"
if (score > highScore) {
highScore = score
salvarHighScore()
Toast.makeText(this, "NOVO RECORDE! 🏆", Toast.LENGTH_LONG).show()
}
binding.btnStart.text = "TENTAR NOVAMENTE"
binding.btnStart.visibility = View.VISIBLE
val botoes = listOf(binding.btnVerde, binding.btnVermelho, binding.btnAmarelo, binding.btnAzul)
botoes.forEach { it.isEnabled = false }
}
private fun atualizarPlacar() {
binding.txtScore.text = "LEVEL: $score" binding.txtHighScore.text = "BEST:$highScore"
}
private fun salvarHighScore() {
val pref = getPreferences(Context.MODE_PRIVATE)
pref.edit().putInt("HIGH_SCORE", highScore).apply()
atualizarPlacar()
}
}
drawable/)app/src/main/res/drawable/quadrant_green.xml)<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:startColor="#66BB6A"
android:endColor="#1B5E20"
android:centerX="0.3"
android:centerY="0.3" />
<corners
android:topLeftRadius="150dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="40dp"
android:bottomRightRadius="10dp" />
<stroke android:width="2dp" android:color="#00000033" />
</shape>
app/src/main/res/drawable/quadrant_red.xml)<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:startColor="#EF5350"
android:endColor="#B71C1C"
android:centerX="0.7"
android:centerY="0.3" />
<corners
android:topRightRadius="150dp"
android:topLeftRadius="40dp"
android:bottomRightRadius="40dp"
android:bottomLeftRadius="10dp" />
<stroke android:width="2dp" android:color="#00000033" />
</shape>
app/src/main/res/drawable/quadrant_yellow.xml)<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:startColor="#FFEE58"
android:endColor="#F57F17"
android:centerX="0.3"
android:centerY="0.7" />
<corners
android:bottomLeftRadius="150dp"
android:topLeftRadius="40dp"
android:bottomRightRadius="40dp"
android:topRightRadius="10dp" />
<stroke android:width="2dp" android:color="#00000033" />
</shape>
app/src/main/res/drawable/quadrant_blue.xml)<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="radial"
android:gradientRadius="200dp"
android:startColor="#42A5F5"
android:endColor="#0D47A1"
android:centerX="0.7"
android:centerY="0.7" />
<corners
android:bottomRightRadius="150dp"
android:topRightRadius="40dp"
android:bottomLeftRadius="40dp"
android:topLeftRadius="10dp" />
<stroke android:width="2dp" android:color="#00000033" />
</shape>
app/src/main/res/drawable/center_circle.xml)<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#1A1A1A"/>
<stroke android:width="4dp" android:color="#FFFFFF"/>
</shape>
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd android_p03_genius_classic
./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/android_p03_genius_classic