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:
-
Fundação e Segurança (Partes 1, 2 e 5): Tela de Login, Registro de Moradores com CPF, AuthBloc, e UX limpa.
-
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.
-
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.
-
UX Imersiva e Navegação (Parte 6): Roteamento em árvore com
StatefulShellRoutedo 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! 🚀📱