🏆 P10: Arena Social: O SuperApp Gamer (Firebase Suite)
Seja muito bem-vindo ao BOSS FINAL da sua jornada de desenvolvimento móvel! 🏆
Este projeto marca o encerramento da Fase 5: Desafio Final do Núcleo Essencial. Aqui, você integrará todo o conhecimento obtido nas fases anteriores (Interface Declarativa, Estados, Listas Recicláveis, Efeitos Animados) e elevará seu aplicativo ao patamar de produção real na nuvem através do ecossistema do Google Firebase.
Desenvolveremos o Arena Social, uma rede social móvel completa onde treinadores criam contas criptografadas, compartilham conquistas e duelam em um feed atualizado instantaneamente via banco de dados reativo no servidor global!
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 📘 Cap 18: Alertas de Batalha (Notificações)
- 📘 Cap 19: Cadastro de Treinador (Formulários)
- 📘 Cap 20: Login na Arena (Auth e Tokens)
- 🏗️ Projeto anterior: P09: PokeMap
🎯 Objetivo do Projeto
Construir uma rede social multi-telas de tema escuro premium neon, acoplada aos servidores em nuvem do Firebase:
- Controle de Acesso por Criptografia: Tela de login e cadastro totalmente integrada ao Firebase Authentication.
- Mural Global em Tempo Real: Feed social de posts roláveis alimentado pelo banco de dados NoSQL Cloud Firestore. Quando qualquer jogador no planeta publica uma nova mensagem desafiando outros treinadores, o mural de todos os usuários do app se atualiza na tela de forma instantânea sem puxar para atualizar (pull-to-refresh).
- Navegação Scaffold Premium: Menu inferior de navegação rápida com componentes de ícone do Material Design 3 e Scaffold estrutural.
graph TD
A[Usuário abre App] --> B{Sessão ativa Firebase Auth?}
B -- Não --> C[Tela de Login/Cadastro]
C -->|Autentica com E-mail e Senha| D[Firebase Auth Server]
D -->|Retorna token de sucesso| E[Mudar para Tela de Feed]
B -- Sim --> E
E --> F[Scaffold: TopBar + Feed List + Bottom Navigation]
F --> G[Firestore addSnapshotListener: Canal em tempo real]
G -->|Monitora modificações na nuvem| H[Reciclar posts com LazyColumn + PostCard]
F -->|Clique em Postar| I[Salva novo post no Firestore Database]
I -->|Grava documento na nuvem| J[(Cloud Firestore NoSQL Server)]
J -->|Notifica todos os aparelhos ativos| G
📖 Dicionário do Projeto
- Google Firebase: A plataforma de infraestrutura em nuvem líder de mercado do Google para aplicativos móveis. Ela oferece banco de dados, servidores de autenticação, armazenamento de mídias e servidores de notificação prontos para uso sem necessidade de programar o backend do zero.
- Firebase Authentication: Serviço especializado que gerencia logins, cadastros, redefinições de senha e criptografia de credenciais de usuários com o máximo de segurança.
- Cloud Firestore: Um banco de dados escalável NoSQL na nuvem em tempo real (Realtime Database) que armazena os dados do aplicativo em formato de Coleções e Documentos (chave-valor estruturados em JSON).
- addSnapshotListener: Um escutador ativo (Websocket) fornecido pelo SDK do Firestore. Ele se mantém conectado ao banco de dados na nuvem e empurra novos dados para o aplicativo no exato milissegundo em que uma gravação acontece no servidor.
- Scaffold: O componente base de tela do Jetpack Compose que implementa o padrão clássico de layout de aplicativos Android, organizando automaticamente a barra de título superior (
TopAppBar), o conteúdo do meio e o menu de abas inferior (NavigationBar). - google-services.json: O arquivo mestre gerado pelo console do Firebase que contém todas as chaves criptográficas do seu servidor. Ele deve ser colado dentro da pasta raiz
app/do seu projeto Android Studio para autorizar a conexão com a nuvem.
🛠️ Passo 1: Configurando o Servidor no Firebase Console
Para dar vida ao SuperApp, precisamos registrar nosso aplicativo no console do desenvolvedor:
- Acesse o Firebase Console.
- Clique em Adicionar projeto, escolha um nome (ex:
Arena Social Gamer) e confirme. - No painel do projeto, clique no ícone do Android para registrar o aplicativo:
- Package Name:
br.com.curso.arenasocial(Deve ser idêntico ao do seu projeto no Android Studio!).
- Package Name:
- Baixe o arquivo
google-services.jsongerado. - Instalação: No Android Studio, mude a visualização do painel esquerdo para
Project, abra a pasta do seu projeto e cole o arquivogoogle-services.jsondentro da pastaapp/.
🛠️ Passo 2: Ativando Serviços no Console do Firebase
Antes de programar, ative os dois motores em nuvem que usaremos:
- Authentication: No menu lateral esquerdo do console, acesse Build > Authentication, clique em Começar e ative o provedor E-mail/Senha como método de login.
- Firestore: Acesse Build > Firestore Database, clique em Criar banco de dados, selecione a localização e inicie em Modo de Teste (para liberar a gravação pública de testes iniciais).
🛠️ Passo 3: Configurando Dependências do Firebase (Gradle)
- Abra o arquivo
build.gradle (Project :...)da raiz do projeto e configure o plugin de serviços do Google no bloco buildscript:buildscript { dependencies { classpath 'com.google.gms:google-services:4.4.0' } } - Abra o arquivo
build.gradle (Module :app)e aplique o plugin logo após os plugins do Kotlin:plugins { ... id 'com.google.gms.google-services' } - Adicione os SDKs do Firebase no bloco
dependencies:dependencies { // Import do Firebase BoM (Bill of Materials) implementation platform('com.google.firebase:firebase-bom:32.7.0') // Firebase Authentication e Firestore implementation 'com.google.firebase:firebase-auth-ktx' implementation 'com.google.firebase:firebase-firestore-ktx' ... }Clique em Sync Now.
🎨 Passo 4: Criando a Tela de Login e Conexão na Nuvem
Abra sua MainActivity.kt em app > src > main > java > br > com > curso > arenasocial > MainActivity.kt.
Implementaremos o fluxo de autenticação real. Para evitar falhas críticas de compilação ou fechamentos repentinos (crashes) antes que o estudante baixe o seu próprio google-services.json, o código do Firebase vem fornecido no Gabarito com blocos estratégicos comentados. Você poderá descomentá-los e testar com login e senha reais!
⚙️ Passo 5: Criando o Mural Reativo em Tempo Real (Firestore)
Na TelaFeed, configuramos um LaunchedEffect contendo o ouvinte ativo do Firestore db.collection("mural").addSnapshotListener { snapshot, e -> ... }. A cada post inserido por outros usuários no banco de dados na nuvem, este bloco é executado, limpa a nossa lista reativa e recarrega os dados fresquinhos na tela do celular de forma instantânea!
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Curtidas no Mural: Adicione um botão de like que incrementa um contador no Firestore.
- Notificação de Novo Post: Dispare uma notificação local quando o listener do Firestore detectar um novo post no mural em tempo real.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
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.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.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
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), // Rosa Neon
secondary = Color(0xFF9C27B0),
background = Color(0xFF0A0A0A),
surface = Color(0xFF1E1E1E)
),
content = content
)
}
@Composable
fun ArenaSocialApp() {
var telaAtual by remember { mutableStateOf("login") }
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
when (telaAtual) {
"login" -> TelaLogin { telaAtual = "feed" }
"feed" -> TelaFeed()
}
}
}
@Composable
fun TelaLogin(aoLogar: () -> Unit) {
val context = LocalContext.current
var email by remember { mutableStateOf("treinador@curso.com") }
var senha by remember { mutableStateOf("123456") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(24.dp))
.background(Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFF9C27B0)))),
contentAlignment = Alignment.Center
) {
Text("⚔️", fontSize = 50.sp)
}
Spacer(modifier = Modifier.height(40.dp))
Text("ARENA SOCIAL", fontSize = 32.sp, fontWeight = FontWeight.Black, color = Color.White)
Text("A rede social definitiva para gamers", fontSize = 14.sp, color = Color.Gray)
Spacer(modifier = Modifier.height(48.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("E-mail do Treinador") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFFE91E63),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = senha,
onValueChange = { senha = it },
label = { Text("Senha") },
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFFE91E63),
unfocusedTextColor = Color.White,
focusedTextColor = Color.White
)
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
// GABARITO COMENTADO: Descomente para usar o Firebase real!
/*
val auth = FirebaseAuth.getInstance()
auth.signInWithEmailAndPassword(email, senha)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
aoLogar()
} else {
Toast.makeText(context, "Erro no login: ${task.exception?.message}", Toast.LENGTH_SHORT).show()
}
}
*/
// Enquanto não tem Firebase, entra direto:
aoLogar()
},
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63))
) {
Text("ENTRAR NA ARENA", fontWeight = FontWeight.Bold, color = Color.White)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaFeed() {
val postagens = remember {
mutableStateListOf(
PostData("Ash Ketchum", "10m", "Desafio qualquer um para uma batalha de ginásio! ⚡", 150),
PostData("Gary Oak", "2h", "Capturei um Blastoise nível 50 sem esforço. 😎", 89),
PostData("Misty", "4h", "Alguém para trocar Pokémons de água?", 45)
)
}
LaunchedEffect(Unit) {
// GABARITO COMENTADO: Descomente para carregar do Firestore real!
/*
val db = FirebaseFirestore.getInstance()
db.collection("mural")
.addSnapshotListener { snapshot, e ->
if (e != null) return@addSnapshotListener
if (snapshot != null) {
postagens.clear()
for (doc in snapshot.documents) {
val user = doc.getString("usuario") ?: "Desconhecido"
val content = doc.getString("mensagem") ?: ""
postagens.add(PostData(user, "Agora", content, 0))
}
}
}
*/
}
Scaffold(
containerColor = Color(0xFF0A0A0A),
topBar = {
CenterAlignedTopAppBar(
title = { Text("MURAL DE BATALHAS", fontWeight = FontWeight.ExtraBold, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color(0xFF1E1E1E),
titleContentColor = Color.White
)
)
},
bottomBar = {
NavigationBar(containerColor = Color(0xFF1E1E1E)) {
NavigationBarItem(selected = true, onClick = {}, icon = { Icon(Icons.Default.Home, null) }, label = { Text("Mural") })
NavigationBarItem(selected = false, onClick = {}, icon = { Icon(Icons.Default.Add, null) }, label = { Text("Desafiar") })
NavigationBarItem(selected = false, onClick = {}, icon = { Icon(Icons.Default.Person, null) }, label = { Text("Perfil") })
}
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding).fillMaxSize()) {
items(postagens) { post ->
PostCard(post)
}
}
}
}
data class PostData(val user: String, val time: String, val content: String, val likes: Int)
@Composable
fun PostCard(post: PostData) {
Card(
modifier = Modifier.padding(12.dp).fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
shape = RoundedCornerShape(20.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(40.dp).clip(CircleShape).background(Color.DarkGray))
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(post.user, fontWeight = FontWeight.Bold, color = Color.White)
Text(post.time, fontSize = 12.sp, color = Color.Gray)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(post.content, color = Color.White, fontSize = 16.sp)
Spacer(modifier = Modifier.height(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Favorite, null, tint = Color(0xFFE91E63), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("${post.likes} curtidas", color = Color.Gray, fontSize = 14.sp)
}
}
}
}