Construindo um App para Condomínios com Flutter 3.41: Roteamento Avançado, Perfil e Credencial (Final)

Ao longo desta série, construímos uma arquitetura sólida com Flutter 3.41, BLoC e Material 3. Criamos sistemas de autenticação, reservas de áreas comuns, murais de avisos e um menu flutuante personalizado (Glassmorphism).

O grande desafio de menus de navegação customizados não é desenhá-los, mas fazê-los funcionar sem destruir o estado das telas (ex: não perder a posição da rolagem da Home ao ir para o Perfil e voltar). Vamos resolver isso usando o poder do GoRouter.


🔀 Passo 1: Conectando o Menu Flutuante (StatefulShellRoute)

Na Parte 5, criamos a UI do FloatingNavBar. Agora, vamos integrá-lo ao GoRouter usando o StatefulShellRoute. Isso cria “ramificações” (branches) independentes de navegação para Home, Histórico e Perfil.

Atualize o app_router.dart:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Importe suas telas...

final rootNavigatorKey = GlobalKey<NavigatorState>();
final sectionNavigatorKey = GlobalKey<NavigatorState>();

final goRouter = GoRouter(
  navigatorKey: rootNavigatorKey,
  initialLocation: '/home',
  routes: [
    // Rotas de Auth (Fora do Menu)
    GoRoute(path: '/auth', builder: (context, state) => const LoginScreen()),
    
    // Rota com Menu Persistente
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        // Retorna o MainLayout passando o navigationShell
        return MainLayout(navigationShell: navigationShell);
      },
      branches: [
        // Branch 0: Home
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeScreen(),
            ),
          ],
        ),
        // Branch 1: Histórico
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/history',
              builder: (context, state) => const HistoryScreen(), // Tela criada na Parte 3
            ),
          ],
        ),
        // Branch 2: Perfil
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              builder: (context, state) => const ProfileScreen(), // Criaremos no Passo 2
            ),
          ],
        ),
      ],
    ),
  ],
);

Atualize o main_layout.dart para usar o NavigationShell:

O MainLayout agora gerencia a troca de abas comunicando-se diretamente com o roteador.

class MainLayout extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const MainLayout({super.key, required this.navigationShell});

  void _goBranch(int index) {
    navigationShell.goBranch(
      index,
      // Suporte para voltar à raiz da aba ao clicar nela novamente
      initialLocation: index == navigationShell.currentIndex,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBody: true,
      // O body agora é o navigationShell, que gerencia as telas internamente
      body: Stack(
        children: [
          Positioned.fill(child: navigationShell),
          FloatingNavBar(
            currentIndex: navigationShell.currentIndex,
            onTap: _goBranch, // Link direto para as branches do GoRouter
          ),
        ],
      ),
    );
  }
}

Pronto! Agora, ao clicar no Menu Flutuante, o Flutter alterna entre a Home, Histórico e Perfil sem recarregar os dados desnecessariamente.


👤 Passo 2: A Tela de Perfil (Material 3)

A tela de perfil concentra as informações do usuário, configurações do app e o acesso à sua credencial digital. Usaremos listas agrupadas e os cards tonais do M3.

Arquivo: lib/modules/profile/profile_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../blocs/auth/auth_bloc.dart';
import '../../blocs/auth/auth_event.dart';
import 'widgets/resident_qr_modal.dart'; // Criaremos no Passo 3

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Meu Perfil'),
        actions: [
          IconButton(
            icon: const Icon(Icons.qr_code_scanner),
            tooltip: 'Minha Credencial',
            onPressed: () => _showMyQrCode(context),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
        children: [
          // Header do Morador
          Center(
            child: Column(
              children: [
                CircleAvatar(
                  radius: 50,
                  backgroundColor: colorScheme.primaryContainer,
                  child: Icon(Icons.person, size: 50, color: colorScheme.onPrimaryContainer),
                ),
                const SizedBox(height: 16),
                Text('João Silva', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
                Text('Apto 402 - Bloco B', style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: colorScheme.secondary)),
              ],
            ),
          ),
          const SizedBox(height: 32),

          // Seção: Minha Unidade
          Text('Minha Unidade', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.primary)),
          const SizedBox(height: 8),
          Card(
            elevation: 0,
            color: colorScheme.surfaceContainerHighest,
            child: Column(
              children: [
                ListTile(
                  leading: const Icon(Icons.group),
                  title: const Text('Moradores Cadastrados'),
                  trailing: const Icon(Icons.chevron_right),
                  onTap: () {},
                ),
                const Divider(height: 1),
                ListTile(
                  leading: const Icon(Icons.directions_car),
                  title: const Text('Veículos'),
                  trailing: const Icon(Icons.chevron_right),
                  onTap: () {},
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          // Seção: Configurações
          Text('Configurações', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.primary)),
          const SizedBox(height: 8),
          Card(
            elevation: 0,
            color: colorScheme.surfaceContainerHighest,
            child: Column(
              children: [
                SwitchListTile(
                  secondary: const Icon(Icons.notifications),
                  title: const Text('Notificações Push'),
                  value: true,
                  onChanged: (val) {},
                ),
                const Divider(height: 1),
                SwitchListTile(
                  secondary: const Icon(Icons.dark_mode),
                  title: const Text('Tema Escuro'),
                  value: false, // Idealmente ligado a um ThemeBloc
                  onChanged: (val) {},
                ),
              ],
            ),
          ),
          const SizedBox(height: 40),

          // Logout
          FilledButton.tonalIcon(
            onPressed: () {
              // Dispara o evento de Logout no BLoC e redireciona
              context.read<AuthBloc>().add(LogoutRequested());
              context.go('/auth');
            },
            icon: const Icon(Icons.logout),
            label: const Text('SAIR DO APLICATIVO'),
            style: FilledButton.styleFrom(
              foregroundColor: colorScheme.error,
              backgroundColor: colorScheme.errorContainer,
            ),
          ),
          const SizedBox(height: 100), // Espaço para o Menu Flutuante
        ],
      ),
    );
  }

  void _showMyQrCode(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent, // Para o modal ter bordas customizadas
      builder: (context) => const ResidentQrModal(),
    );
  }
}

🪪 Passo 3: Credencial Digital (O “Meu QR Code”)

Diferente do QR Code de Convite (criado na Parte 4), este é o QR Code do Morador, usado para interagir com catracas do condomínio, validar identidade em assembleias ou compartilhar contato interno com outros moradores.

Arquivo: lib/modules/profile/widgets/resident_qr_modal.dart

import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';

class ResidentQrModal extends StatelessWidget {
  const ResidentQrModal({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    
    // String criptografada simulando o ID do morador
    const myAccessString = "RESIDENT|ID-98765|APTO-402|HASH-XYZ"; 

    return Container(
      margin: const EdgeInsets.all(16), // Margem para efeito flutuante
      decoration: BoxDecoration(
        color: colorScheme.surface,
        borderRadius: BorderRadius.circular(28),
      ),
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // Pill Handle
          Container(
            width: 40, height: 4,
            decoration: BoxDecoration(
              color: colorScheme.outlineVariant,
              borderRadius: BorderRadius.circular(2),
            ),
          ),
          const SizedBox(height: 24),
          
          Text('Minha Credencial', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          Text(
            'Aproxime este código do leitor da catraca ou mostre na portaria.',
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const SizedBox(height: 32),
          
          // QR Code em um Container destacado
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(24),
              boxShadow: [
                BoxShadow(
                  color: colorScheme.primary.withOpacity(0.1),
                  blurRadius: 20,
                  spreadRadius: 5,
                )
              ],
            ),
            child: QrImageView(
              data: myAccessString,
              version: QrVersions.auto,
              size: 220.0,
              // Adicionando um logo no meio do QR Code (Opcional, requer a imagem nos assets)
              // embeddedImage: const AssetImage('assets/images/logo_icon.png'),
              // embeddedImageStyle: const QrEmbeddedImageStyle(size: Size(40, 40)),
            ),
          ),
          const SizedBox(height: 32),
          
          // Informações de Segurança M3
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.verified_user, color: colorScheme.primary, size: 20),
              const SizedBox(width: 8),
              Text(
                'Código atualiza automaticamente a cada 60s',
                style: Theme.of(context).textTheme.labelSmall?.copyWith(color: colorScheme.primary),
              ),
            ],
          ),
          const SizedBox(height: 24),
          
          SizedBox(
            width: double.infinity,
            child: FilledButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('FECHAR'),
            ),
          )
        ],
      ),
    );
  }
}

🏆 Conclusão do Projeto (Visão do Arquiteto)

Com a finalização desta sexta parte, transformamos a ideia de um “App de Condomínio” em um Software Enterprise em Flutter.

O que você construiu:

  1. Fundação e Segurança (Partes 1, 2 e 5): Tela de Login, Registro de Moradores com CPF, AuthBloc, e UX limpa.

  2. Gerenciamento de Estado de Alta Escala (Parte 3): Substituição de estados efêmeros pelo BLoC Pattern, separando rigorosamente a UI das regras de negócio.

  3. IoT, Destaques e Agendamentos (Parte 4): Lógica complexa de horários para churrasqueiras/salões, e botões Secure Action para abertura de portões, incluindo geração de QR Code de convidados.

  4. UX Imersiva e Navegação (Parte 6): Roteamento em árvore com StatefulShellRoute do GoRouter, mantendo o menu flutuante Glassmorphism sincronizado perfeitamente com a Home, o Histórico e a nova tela de Perfil.

O Flutter 3.41 e o Material 3 provaram ser a combinação perfeita para criar aplicações que não são apenas performáticas, mas também belas e responsivas.

Este código base está pronto para receber integrações reais (APIs REST/GraphQL, Firebase, WebSockets para catracas). Sinta-se orgulhoso desta arquitetura.

Feliz codificação, e sucesso nos seus próximos deploys! 🚀📱

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20