🏆 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:


🎯 Objetivo do Projeto

Construir uma rede social multi-telas de tema escuro premium neon, acoplada aos servidores em nuvem do Firebase:

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


🛠️ Passo 1: Configurando o Servidor no Firebase Console

Para dar vida ao SuperApp, precisamos registrar nosso aplicativo no console do desenvolvedor:

  1. Acesse o Firebase Console.
  2. Clique em Adicionar projeto, escolha um nome (ex: Arena Social Gamer) e confirme.
  3. 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!).
  4. Baixe o arquivo google-services.json gerado.
  5. Instalação: No Android Studio, mude a visualização do painel esquerdo para Project, abra a pasta do seu projeto e cole o arquivo google-services.json dentro da pasta app/.

🛠️ Passo 2: Ativando Serviços no Console do Firebase

Antes de programar, ative os dois motores em nuvem que usaremos:

  1. 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.
  2. 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)

  1. 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'
    }
    }
    
  2. 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'
    }
    
  3. 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:


📖 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)
            }
        }
    }
}