📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Seja muito bem-vindo ao GRANDE FINAL da sua jornada de desenvolvimento móvel nativo com Android e Jetpack Compose! 🏆
Neste projeto de encerramento da Fase 5: Desafio Final do Núcleo Essencial, você integrará todo o conhecimento obtido nas fases anteriores (Interface Declarativa, Gerenciamento de Estados, Listas Recicláveis com LazyColumn, Navegação com Scaffold, Diálogos e Interações) e elevará seu aplicativo ao patamar de produção profissional na nuvem através do ecossistema do Google Firebase.
Desenvolveremos o Arena Social, uma rede social móvel completa com tema cyberpunk neon onde treinadores criam contas, compartilham desafios de batalha e acompanham um mural atualizado instantaneamente via Cloud Firestore (Realtime Database) e Firebase Authentication!
| 1. Login & Firebase Auth | 2. Mural em Tempo Real | 3. Publicar Desafio | 4. Perfil & Conquistas |
|---|---|---|---|
| Autenticação segura com e-mail e senha. | Feed rolável reativo com curtidas dinâmicas. | Diálogo com publicação instantânea no Firestore. | Estatísticas de vitórias, insígnias e logout. |
Antes de iniciar este laboratório de fechamento, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
google-services.json e declarar o Firebase BoM (Bill of Materials).signInWithEmailAndPassword / createUserWithEmailAndPassword).addSnapshotListener) para receber atualizações instantâneas sem necessidade de puxar para atualizar (pull-to-refresh).LazyColumn com chaves únicas (key = { it.id }), cards estilizados com gradientes e botões de curtida com mutação de estado.AlertDialog): Capturar entradas de texto de novos posts e salvar coleções no Firestore.graph TD
A[Usuário abre Arena Social] --> B{Sessão ativa ou Login realizado?}
B -- Não --> C[TelaLogin: Inputs de E-mail e Senha]
C --> D[Usuário clica em Entrar na Arena / Criar Conta]
D --> E{Firebase Auth ou Sandbox Local?}
E -- Firebase Real --> F[FirebaseAuth.signInWithEmailAndPassword]
E -- Modo Sandbox --> G[Login Imediato com Usuário Local]
F --> H{Autenticação Válida?}
H -- Sim --> I[Navega para TelaFeed]
H -- Não --> J[Exibe Toast com mensagem de erro do Firebase]
G --> I
I --> K[Scaffold: TopAppBar + Feed LazyColumn + NavigationBar]
K --> L[Firestore addSnapshotListener: Monitoramento Realtime]
L --> M[Recebe Documentos da Coleção 'mural']
M --> N[Renderiza Lista de PostCards com Curtidas]
K --> O{Ação na NavigationBar?}
O -- Tocar em '+' Desafiar --> P[Abre AlertDialog: Novo Desafio]
O -- Tocar em Perfil --> Q[Navega para TelaPerfil: Conquistas & Logout]
P --> R[Usuário digita texto e clica em PUBLICAR]
R --> S[Firestore db.collection('mural').add(...) & Atualiza Feed Local]
S --> L
Q --> T[Exibe Estatísticas: Vitórias, Insígnias, Desafios]
Q --> U[Botão Desconectar: Reseta Sessão e Retorna para TelaLogin]
| Termo / Componente | O que é e para que serve? |
|---|---|
| Google Firebase | Plataforma completa de infraestrutura em nuvem do Google que fornece autenticação, bancos de dados em tempo real, analytics e cloud storage para aplicativos. |
| Firebase BoM | Bill of Materials do Firebase. Gerencia automaticamente as versões compatíveis de todas as bibliotecas do ecossistema Firebase sem conflitos de dependências. |
| Firebase Authentication | Motor de controle de acesso que realiza a validação criptografada de usuários, tokens de sessão e gerenciamento de permissões. |
| Cloud Firestore | Banco de dados NoSQL flexível e escalável baseado em documentos (JSON-like) e coleções, com sincronização em tempo real via websockets. |
| addSnapshotListener | Ouvinte reativo do Firestore que dispara um callback sempre que um documento na nuvem for criado, modificado ou excluído. |
| google-services.json | Arquivo de configuração baixado do Firebase Console que contém o project_id, chaves de API e certificados necessários para a comunicação segura com o servidor. |
| Scaffold | Estrutura de layout base do Material 3 que organiza cabeçalho (topBar), barra de navegação (bottomBar) e conteúdo rolável (content). |
File > New > New Project).Arena Social (ou android_p10_social_firebase)br.com.curso.arenasocial (Atenção: deve ser exatamente este para coincidir com o Firebase!)KotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)Arena Social Gamer e confirme.br.com.curso.arenasocialArena Socialgoogle-services.json.Android para Project.google-services.json diretamente dentro da pasta app/ (ao lado do build.gradle do módulo).
southamerica-east1 em SP) e selecione Iniciar no modo de teste (para liberar permissões públicas durante os testes em sala de aula).build.gradle (Project: ...):
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
app/build.gradle:
```groovy
plugins {
id ‘com.android.application’
id ‘org.jetbrains.kotlin.android’
id ‘com.google.gms.google-services’ // Plugin do Google Services
}dependencies { implementation platform(‘androidx.compose:compose-bom:2024.02.00’) implementation ‘androidx.compose.ui:ui’ implementation ‘androidx.compose.ui:ui-graphics’ implementation ‘androidx.compose.ui:ui-tooling-preview’ implementation ‘androidx.compose.material3:material3’ implementation ‘androidx.activity:activity-compose:1.8.2’ implementation ‘androidx.core:core-ktx:1.12.0’ implementation ‘androidx.lifecycle:lifecycle-runtime-ktx:2.7.0’
// Firebase BoM e Bibliotecas Oficiais
implementation platform('com.google.firebase:firebase-bom:32.7.2')
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.firebase:firebase-firestore-ktx' } ``` *Clique em **Sync Now**.*
PostData)Abra app > src > main > java > br > com > curso > arenasocial > MainActivity.kt:
data class PostData(
val id: String = "",
val user: String,
val time: String,
val content: String,
var likes: Int = 0,
var isLiked: Boolean = false
)
ArenaSocialTheme)@Composable
fun ArenaSocialTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFE91E63), // Rosa Neon
secondary = Color(0xFF9C27B0), // Roxo Elétrico
tertiary = Color(0xFF00E676), // Verde Destaque
background = Color(0xFF0A0A0A), // Preto Profundo
surface = Color(0xFF161616),
surfaceVariant = Color(0xFF222222)
),
content = content
)
}
TelaLogin)A tela de login possui validação visual, suporte a alternar entre “ENTRAR” e “CRIAR CONTA” e opera de maneira resiliente:
@Composable
fun TelaLogin(aoLogar: (String) -> Unit) {
val context = LocalContext.current
var email by remember { mutableStateOf("treinador@curso.com") }
var senha by remember { mutableStateOf("123456") }
var isCadastro by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxSize().padding(28.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(24.dp))
.background(Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))),
contentAlignment = Alignment.Center
) {
Text("⚔️", fontSize = 48.sp)
}
Spacer(modifier = Modifier.height(28.dp))
Text("ARENA SOCIAL", fontSize = 30.sp, fontWeight = FontWeight.Black, color = Color.White, letterSpacing = 2.sp)
Text(if (isCadastro) "Crie sua conta de Treinador" else "A rede social definitiva para gamers", fontSize = 13.sp, color = Color.Gray)
Spacer(modifier = Modifier.height(36.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("E-mail do Treinador") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(14.dp))
OutlinedTextField(
value = senha,
onValueChange = { senha = it },
label = { Text("Senha") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
val nomeTreinador = email.substringBefore("@").replaceFirstChar { it.uppercase() }
// GABARITO: Descomente para usar Firebase Auth Real!
/*
val auth = FirebaseAuth.getInstance()
if (isCadastro) {
auth.createUserWithEmailAndPassword(email, senha).addOnCompleteListener { task ->
if (task.isSuccessful) aoLogar(nomeTreinador)
else Toast.makeText(context, "Erro: ${task.exception?.message}", Toast.LENGTH_LONG).show() } } else { auth.signInWithEmailAndPassword(email, senha).addOnCompleteListener { task -> if (task.isSuccessful) aoLogar(nomeTreinador) else Toast.makeText(context, "Erro:${task.exception?.message}", Toast.LENGTH_LONG).show()
}
}
*/
// Modo Sandbox (Entrada Imediata para Teste):
Toast.makeText(context, "Bem-vindo à Arena, $nomeTreinador!", Toast.LENGTH_SHORT).show()
aoLogar(nomeTreinador)
},
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63))
) {
Text(if (isCadastro) "CRIAR CONTA" else "ENTRAR NA ARENA", fontWeight = FontWeight.Black, color = Color.White)
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = { isCadastro = !isCadastro }) {
Text(if (isCadastro) "Já possui conta? Faça Login" else "Não tem conta? Cadastre-se", color = Color(0xFF9C27B0))
}
}
}
TelaFeed)O feed é renderizado através de uma LazyColumn. Quando um usuário clica no coração de um post, o estado local é atualizado reativamente:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaFeed(usuario: String, aoIrParaPerfil: () -> Unit) {
val postagens = remember {
mutableStateListOf(
PostData("1", "Ash Ketchum", "10m", "Desafio qualquer um para uma batalha no Ginásio Central! ⚡", 150, false),
PostData("2", "Gary Oak", "2h", "Capturei um Blastoise nível 50 sem esforço na rota 12. 😎", 89, false),
PostData("3", "Misty", "4h", "Alguém em Cerulean para trocar Pokémons de água hoje?", 45, false)
)
}
var mostrarDialogoPost by remember { mutableStateOf(false) }
// Listener Realtime com Firestore
LaunchedEffect(Unit) {
/*
val db = FirebaseFirestore.getInstance()
db.collection("mural").addSnapshotListener { snapshot, e ->
if (e != null || snapshot == null) return@addSnapshotListener
postagens.clear()
for (doc in snapshot.documents) {
val user = doc.getString("usuario") ?: "Treinador"
val content = doc.getString("mensagem") ?: ""
val likes = doc.getLong("likes")?.toInt() ?: 0
postagens.add(PostData(doc.id, user, "Agora", content, likes, false))
}
}
*/
}
Scaffold(
containerColor = Color(0xFF0A0A0A),
topBar = {
CenterAlignedTopAppBar(
title = { Text("MURAL DE BATALHAS", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF161616), titleContentColor = Color.White)
)
},
bottomBar = {
NavigationBar(containerColor = Color(0xFF161616)) {
NavigationBarItem(selected = true, onClick = {}, icon = { Icon(Icons.Default.Home, null) }, label = { Text("Mural") })
NavigationBarItem(selected = false, onClick = { mostrarDialogoPost = true }, icon = { Icon(Icons.Default.Add, null) }, label = { Text("Desafiar") })
NavigationBarItem(selected = false, onClick = aoIrParaPerfil, icon = { Icon(Icons.Default.Person, null) }, label = { Text("Perfil") })
}
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding).fillMaxSize()) {
items(postagens, key = { it.id }) { post ->
PostCard(
post = post,
onToggleLike = {
val index = postagens.indexOf(post)
if (index != -1) {
val liked = !post.isLiked
val newLikes = if (liked) post.likes + 1 else (post.likes - 1).coerceAtLeast(0)
postagens[index] = post.copy(likes = newLikes, isLiked = liked)
}
}
)
}
}
}
}
AlertDialog)Ao tocar no botão de + Desafiar na barra de navegação, o aplicativo exibe uma janela modal para digitar a mensagem e enviá-la para o banco de dados na nuvem:
if (mostrarDialogoPost) {
AlertDialog(
onDismissRequest = { mostrarDialogoPost = false },
containerColor = Color(0xFF1E1E1E),
title = { Text("⚔️ Novo Desafio no Mural", fontWeight = FontWeight.Bold, color = Color.White) },
text = {
Column {
Text("Publique um desafio ou conquista para toda a comunidade gamer!", fontSize = 13.sp, color = Color.Gray)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = novoPostTexto,
onValueChange = { novoPostTexto = it },
placeholder = { Text("Ex: Procuro oponente nível 40!") },
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
Button(
onClick = {
if (novoPostTexto.isNotBlank()) {
postagens.add(0, PostData(System.currentTimeMillis().toString(), usuario, "Agora", novoPostTexto.trim(), 0, false))
// GABARITO: Gravação no Firestore Real
/*
val db = FirebaseFirestore.getInstance()
db.collection("mural").add(mapOf("usuario" to usuario, "mensagem" to novoPostTexto.trim(), "likes" to 0, "criadoEm" to System.currentTimeMillis()))
*/
novoPostTexto = ""
mostrarDialogoPost = false
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63))
) {
Text("PUBLICAR", fontWeight = FontWeight.Bold, color = Color.White)
}
},
dismissButton = {
TextButton(onClick = { mostrarDialogoPost = false }) { Text("Cancelar", color = Color.Gray) }
}
)
}
TelaPerfil)Exibe as estatísticas do jogador logado, suas conquistas e permite desconectar da sessão:
@Composable
fun TelaPerfil(usuario: String, aoVoltarMural: () -> Unit, aoDesconectar: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier.size(100.dp).clip(CircleShape).background(Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))),
contentAlignment = Alignment.Center
) {
Text(usuario.firstOrNull()?.uppercase() ?: "T", fontSize = 44.sp, fontWeight = FontWeight.Black, color = Color.White)
}
Spacer(modifier = Modifier.height(16.dp))
Text(usuario, fontSize = 24.sp, fontWeight = FontWeight.Black, color = Color.White)
Text("Treinador Oficial da Arena", fontSize = 13.sp, color = Color(0xFF00E676))
Spacer(modifier = Modifier.height(28.dp))
Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color(0xFF161616)), shape = RoundedCornerShape(16.dp)) {
Row(modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalArrangement = Arrangement.SpaceAround) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("34", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFFE91E63))
Text("Vitórias", fontSize = 12.sp, color = Color.Gray)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("8", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFF9C27B0))
Text("Insígnias", fontSize = 12.sp, color = Color.Gray)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("142", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFF00E676))
Text("Desafios", fontSize = 12.sp, color = Color.Gray)
}
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = aoVoltarMural, modifier = Modifier.fillMaxWidth().height(50.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E1E1E)), shape = RoundedCornerShape(12.dp)) {
Text("VOLTAR AO MURAL", color = Color.White, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(12.dp))
OutlinedButton(onClick = aoDesconectar, modifier = Modifier.fillMaxWidth().height(50.dp), shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFFF1744))) {
Text("DESCONECTAR (LOGOUT)", fontWeight = FontWeight.Bold)
}
}
}
| Sintoma Observado | Causa Provável | Como Resolver |
|---|---|---|
File google-services.json is missing |
O arquivo de configuração do Firebase não foi colado na pasta correta. | Mude a visualização do Android Studio para Project, abra a pasta do projeto e cole o google-services.json diretamente dentro da subpasta app/. |
PERMISSION_DENIED: Missing or insufficient permissions |
As regras do Cloud Firestore estão bloqueando leituras ou gravações. | No Firebase Console, vá em Firestore Database > Regras (Rules) e altere para allow read, write: if true; para testes em laboratório. |
The email address is badly formatted |
O e-mail digitado no campo de login não segue o formato padrão (nome@dominio.com). |
Certifique-se de preencher um e-mail válido com @ e domínio. |
The password must be 6 characters long or more |
O Firebase Auth exige senhas com no mínimo 6 caracteres. | Digite uma senha com pelo menos 6 dígitos. |
| O aplicativo roda perfeitamente sem o Firebase configurado? | O app foi projetado com Modo Sandbox Local com mocks reativos. | Sim! Você pode testar todas as transições de tela, likes e publicações de posts no emulador mesmo antes de criar o projeto no Firebase Console. |
FieldValue.increment(1) na coleção mural do Firestore para sincronizar o contador de curtidas com todos os celulares conectados ao mesmo tempo!Disponível em: app/src/main/java/br/com/curso/arenasocial/MainActivity.kt
package br.com.curso.arenasocial
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
data class PostData(
val id: String = "",
val user: String,
val time: String,
val content: String,
var likes: Int = 0,
var isLiked: Boolean = false
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ArenaSocialTheme {
ArenaSocialApp()
}
}
}
}
@Composable
fun ArenaSocialTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFE91E63),
secondary = Color(0xFF9C27B0),
tertiary = Color(0xFF00E676),
background = Color(0xFF0A0A0A),
surface = Color(0xFF161616),
surfaceVariant = Color(0xFF222222)
),
content = content
)
}
@Composable
fun ArenaSocialApp() {
var telaAtual by remember { mutableStateOf("login") }
var usuarioLogado by remember { mutableStateOf("Treinador(a)") }
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
when (telaAtual) {
"login" -> TelaLogin(
aoLogar = { nome ->
usuarioLogado = nome
telaAtual = "feed"
}
)
"feed" -> TelaFeed(
usuario = usuarioLogado,
aoIrParaPerfil = { telaAtual = "perfil" }
)
"perfil" -> TelaPerfil(
usuario = usuarioLogado,
aoVoltarMural = { telaAtual = "feed" },
aoDesconectar = {
usuarioLogado = "Treinador(a)"
telaAtual = "login"
}
)
}
}
}
@Composable
fun TelaLogin(aoLogar: (String) -> Unit) {
val context = LocalContext.current
var email by remember { mutableStateOf("treinador@curso.com") }
var senha by remember { mutableStateOf("123456") }
var isCadastro by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(28.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(24.dp))
.background(Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))),
contentAlignment = Alignment.Center
) {
Text("⚔️", fontSize = 48.sp)
}
Spacer(modifier = Modifier.height(28.dp))
Text(
text = "ARENA SOCIAL",
fontSize = 30.sp,
fontWeight = FontWeight.Black,
color = Color.White,
letterSpacing = 2.sp
)
Text(
text = if (isCadastro) "Crie sua conta de Treinador" else "A rede social definitiva para gamers",
fontSize = 13.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.height(36.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("E-mail do Treinador") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFFE91E63),
focusedLabelColor = Color(0xFFE91E63),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
)
)
Spacer(modifier = Modifier.height(14.dp))
OutlinedTextField(
value = senha,
onValueChange = { senha = it },
label = { Text("Senha") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFFE91E63),
focusedLabelColor = Color(0xFFE91E63),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
)
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
val nomeTreinador = email.substringBefore("@").replaceFirstChar { it.uppercase() }
Toast.makeText(context, "Bem-vindo à Arena, $nomeTreinador!", Toast.LENGTH_SHORT).show()
aoLogar(nomeTreinador)
},
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63))
) {
Text(
text = if (isCadastro) "CRIAR CONTA" else "ENTRAR NA ARENA",
fontWeight = FontWeight.Black,
fontSize = 15.sp,
color = Color.White
)
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = { isCadastro = !isCadastro }) {
Text(
text = if (isCadastro) "Já possui conta? Faça Login" else "Não tem conta? Cadastre-se",
color = Color(0xFF9C27B0),
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaFeed(
usuario: String,
aoIrParaPerfil: () -> Unit
) {
val postagens = remember {
mutableStateListOf(
PostData("1", "Ash Ketchum", "10m", "Desafio qualquer um para uma batalha no Ginásio Central! ⚡", 150, false),
PostData("2", "Gary Oak", "2h", "Capturei um Blastoise nível 50 sem esforço na rota 12. 😎", 89, false),
PostData("3", "Misty", "4h", "Alguém em Cerulean para trocar Pokémons de água hoje?", 45, false)
)
}
var mostrarDialogoPost by remember { mutableStateOf(false) }
var novoPostTexto by remember { mutableStateOf("") }
Scaffold(
containerColor = Color(0xFF0A0A0A),
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
"MURAL DE BATALHAS",
fontWeight = FontWeight.Black,
letterSpacing = 2.sp,
fontSize = 18.sp
)
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color(0xFF161616),
titleContentColor = Color.White
)
)
},
bottomBar = {
NavigationBar(containerColor = Color(0xFF161616)) {
NavigationBarItem(
selected = true,
onClick = {},
icon = { Icon(Icons.Default.Home, null) },
label = { Text("Mural") },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = Color(0xFFE91E63),
selectedTextColor = Color(0xFFE91E63),
indicatorColor = Color(0xFFE91E63).copy(alpha = 0.2f)
)
)
NavigationBarItem(
selected = false,
onClick = { mostrarDialogoPost = true },
icon = { Icon(Icons.Default.Add, null) },
label = { Text("Desafiar") }
)
NavigationBarItem(
selected = false,
onClick = aoIrParaPerfil,
icon = { Icon(Icons.Default.Person, null) },
label = { Text("Perfil") }
)
}
}
) { padding ->
LazyColumn(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
items(postagens, key = { it.id }) { post ->
PostCard(
post = post,
onToggleLike = {
val index = postagens.indexOf(post)
if (index != -1) {
val liked = !post.isLiked
val newLikes = if (liked) post.likes + 1 else (post.likes - 1).coerceAtLeast(0)
postagens[index] = post.copy(likes = newLikes, isLiked = liked)
}
}
)
}
}
}
if (mostrarDialogoPost) {
AlertDialog(
onDismissRequest = { mostrarDialogoPost = false },
containerColor = Color(0xFF1E1E1E),
titleContentColor = Color.White,
textContentColor = Color.White,
title = {
Text(
text = "⚔️ Novo Desafio no Mural",
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
},
text = {
Column {
Text(
text = "Publique um desafio ou conquista para toda a comunidade gamer!",
fontSize = 13.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = novoPostTexto,
onValueChange = { novoPostTexto = it },
placeholder = { Text("Ex: Procuro oponente nível 40!") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFFE91E63),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
)
)
}
},
confirmButton = {
Button(
onClick = {
if (novoPostTexto.isNotBlank()) {
val newId = System.currentTimeMillis().toString()
postagens.add(
0,
PostData(newId, usuario, "Agora", novoPostTexto.trim(), 0, false)
)
novoPostTexto = ""
mostrarDialogoPost = false
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63))
) {
Text("PUBLICAR", fontWeight = FontWeight.Bold, color = Color.White)
}
},
dismissButton = {
TextButton(onClick = { mostrarDialogoPost = false }) {
Text("Cancelar", color = Color.Gray)
}
}
)
}
}
@Composable
fun PostCard(
post: PostData,
onToggleLike: () -> Unit
) {
Card(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF161616)),
shape = RoundedCornerShape(18.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(42.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))
),
contentAlignment = Alignment.Center
) {
Text(
text = post.user.firstOrNull()?.uppercase() ?: "T",
fontWeight = FontWeight.Black,
color = Color.White,
fontSize = 16.sp
)
}
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(post.user, fontWeight = FontWeight.Bold, color = Color.White, fontSize = 15.sp)
Text(post.time, fontSize = 11.sp, color = Color.Gray)
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = post.content,
color = Color.White.copy(alpha = 0.95f),
fontSize = 15.sp,
lineHeight = 22.sp
)
Spacer(modifier = Modifier.height(14.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clickable { onToggleLike() }
.padding(vertical = 4.dp, horizontal = 6.dp)
) {
Icon(
imageVector = if (post.isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "Curtir",
tint = if (post.isLiked) Color(0xFFE91E63) else Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "${post.likes} curtidas",
color = if (post.isLiked) Color(0xFFE91E63) else Color.Gray,
fontSize = 13.sp,
fontWeight = if (post.isLiked) FontWeight.Bold else FontWeight.Normal
)
}
}
}
}
@Composable
fun TelaPerfil(
usuario: String,
aoVoltarMural: () -> Unit,
aoDesconectar: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))),
contentAlignment = Alignment.Center
) {
Text(
text = usuario.firstOrNull()?.uppercase() ?: "T",
fontSize = 44.sp,
fontWeight = FontWeight.Black,
color = Color.White
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(usuario, fontSize = 24.sp, fontWeight = FontWeight.Black, color = Color.White)
Text("Treinador Oficial da Arena", fontSize = 13.sp, color = Color(0xFF00E676))
Spacer(modifier = Modifier.height(28.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF161616)),
shape = RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceAround
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("34", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFFE91E63))
Text("Vitórias", fontSize = 12.sp, color = Color.Gray)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("8", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFF9C27B0))
Text("Insígnias", fontSize = 12.sp, color = Color.Gray)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("142", fontWeight = FontWeight.Black, fontSize = 20.sp, color = Color(0xFF00E676))
Text("Desafios", fontSize = 12.sp, color = Color.Gray)
}
}
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = aoVoltarMural,
modifier = Modifier.fillMaxWidth().height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E1E1E)),
shape = RoundedCornerShape(12.dp)
) {
Text("VOLTAR AO MURAL", color = Color.White, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(12.dp))
OutlinedButton(
onClick = aoDesconectar,
modifier = Modifier.fillMaxWidth().height(50.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFFF1744))
) {
Text("DESCONECTAR (LOGOUT)", fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd proj_aplicacoes_full_stack/projetos/android_p10_social_firebase
./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 ou Android Studio, abra o projeto diretamente pela pasta
proj_aplicacoes_full_stack/projetos/android_p10_social_firebase.