Construindo um App Financeiro com Flutter 3.41: Isar, Riverpod e Dashboard Inteligente (Parte 1)
Criar um aplicativo de Controle Financeiro Pessoal exige precisão matemática, carregamento instantâneo e uma interface que transmita segurança ao usuário. Para isso, a combinação de Isar Community (banco de dados local com queries na velocidade da luz) e Riverpod (reatividade à prova de falhas) é imbatível.
🗄️ 1. Modelagem de Dados Financeiros (Schema do Isar)
O coração de um app financeiro é o registro de transações (entradas e saídas). No Isar, criaremos uma coleção otimizada para buscas por datas e status de pagamento.
Arquivo: lib/features/transactions/models/transaction.dart
import 'package:isar/isar.dart';
part 'transaction.g.dart'; // Gerado via build_runner
@collection
class Transaction {
Id id = Isar.autoIncrement;
@Index(type: IndexType.value)
late String title; // Ex: "Conta de Luz", "Salário"
late double amount;
@Index()
late bool isIncome; // true = Ganho (Receita), false = Gasto (Despesa)
@Index()
late DateTime dueDate; // Data de Vencimento / Ocorrência
@Index()
late bool isPaid; // Status de pagamento
}
💧 2. Configurando os Providers (Riverpod)
Precisamos injetar o Isar e criar provedores que calculem automaticamente os totais do mês corrente para abastecer os Cards da Home.
Arquivo: lib/core/providers/finance_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import '../database/isar_service.dart';
import '../../features/transactions/models/transaction.dart';
// 1. Provedor do Banco de Dados
final isarProvider = Provider<IsarService>((ref) => IsarService());
// 2. Provedor de Transações do Mês Corrente
final currentMonthTransactionsProvider = FutureProvider<List<Transaction>>((ref) async {
final isar = await ref.read(isarProvider).db;
final now = DateTime.now();
final startOfMonth = DateTime(now.year, now.month, 1);
final endOfMonth = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
return isar.transactions.filter()
.dueDateBetween(startOfMonth, endOfMonth)
.sortByDueDate()
.findAll();
});
⚡ 3. A Tela Inicial Splash
A Splash Screen carregará o banco de dados de forma assíncrona antes de liberar o acesso ao aplicativo, garantindo que o saldo esteja pronto quando a Home abrir.
Arquivo: lib/features/splash/splash_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/finance_providers.dart';
class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key});
@override
ConsumerState<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends ConsumerState<SplashScreen> {
@override
void initState() {
super.initState();
_loadApp();
}
Future<void> _loadApp() async {
// Inicializa o banco (se houver migrations futuras, ocorrem aqui)
await ref.read(isarProvider).db;
await Future.delayed(const Duration(seconds: 2)); // Efeito visual mínimo
if (mounted) context.go('/home');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.account_balance_wallet, size: 80, color: Colors.white),
const SizedBox(height: 16),
Text('FinançaMaster', style: Theme.of(context).textTheme.headlineLarge?.copyWith(color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(height: 24),
const CircularProgressIndicator(color: Colors.white),
],
),
),
);
}
}
🧭 4. O Layout Principal (NavigationBar M3)
Conforme solicitado, criaremos um menu inferior robusto com 5 opções: Extrato, Minhas Contas, Início (Central), Operações e Menu Geral. Usaremos o NavigationBar do Material 3.
Arquivo: lib/core/layouts/main_layout.dart
import 'package:flutter/material.dart';
import '../../features/home/home_screen.dart';
class MainLayout extends StatefulWidget {
const MainLayout({super.key});
@override
State<MainLayout> createState() => _MainLayoutState();
}
class _MainLayoutState extends State<MainLayout> {
int _currentIndex = 2; // Começa na aba "Início"
final List<Widget> _screens = [
const Placeholder(), // Extrato
const Placeholder(), // Minhas Contas
const HomeScreen(), // Início
const Placeholder(), // Operações
const Placeholder(), // Menu
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) => setState(() => _currentIndex = index),
destinations: const [
NavigationDestination(icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), label: 'Extrato'),
NavigationDestination(icon: Icon(Icons.account_balance_outlined), selectedIcon: Icon(Icons.account_balance), label: 'Contas'),
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Início'),
NavigationDestination(icon: Icon(Icons.swap_horiz_outlined), selectedIcon: Icon(Icons.swap_horiz), label: 'Operações'),
NavigationDestination(icon: Icon(Icons.menu), label: 'Menu'),
],
),
);
}
}
🛠️ 5. A AppBar Interativa (Home)
A AppBar da Home precisa exibir o mês corrente, o calendário e opções extras, mantendo um design limpo.
Snippet (Usado dentro da HomeScreen):
AppBar(
elevation: 0,
scrolledUnderElevation: 0, // Evita a mudança de cor ao rolar (M3)
title: Row(
children: [
const CircleAvatar(radius: 16, child: Icon(Icons.person, size: 20)),
const SizedBox(width: 12),
Text('Olá, João', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
],
),
actions: [
IconButton(icon: const Icon(Icons.calendar_month), onPressed: () { /* Abre seletor de mês */ }),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () { /* Opções extras */ }),
],
)
🔍 6. Busca e Cabeçalho do Mês
Abaixo da AppBar, inserimos a barra de pesquisa arredondada do Material 3 e um cabeçalho elegante indicando o mês atual.
Componente Visual:
Widget _buildHeaderAndSearch(BuildContext context) {
// Lógica para pegar o nome do mês (ex: "Março 2026")
final String currentMonth = "Março 2026";
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SearchBar(
hintText: 'Buscar Transações...',
leading: const Icon(Icons.search),
elevation: const WidgetStatePropertyAll(0), // Fundo plano M3
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainerHighest),
),
const SizedBox(height: 24),
Row(
children: [
const Icon(Icons.calendar_today, size: 20),
const SizedBox(width: 8),
Text(currentMonth, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
],
),
],
),
);
}
💳 7. Resumo Financeiro: Cards de Ganhos e Gastos
Calculamos dinamicamente as entradas e saídas do mês. Usaremos setas indicativas (verde para cima, vermelho para baixo).
Componente Visual:
Widget _buildSummaryCards(BuildContext context, List<Transaction> transactions) {
// Cálculo rápido usando a lista do Riverpod
final double income = transactions.where((t) => t.isIncome).fold(0, (sum, t) => sum + t.amount);
final double expenses = transactions.where((t) => !t.isIncome).fold(0, (sum, t) => sum + t.amount);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Card(
color: Colors.green.shade50,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.arrow_upward, color: Colors.green, size: 16),
SizedBox(width: 4),
Text('Ganhos do Mês', style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
Text('R\$ ${income.toStringAsFixed(2)}', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: Colors.green.shade800)),
],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Card(
color: Colors.red.shade50,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.arrow_downward, color: Colors.red, size: 16),
SizedBox(width: 4),
Text('Gastos do Mês', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
Text('R\$ ${expenses.toStringAsFixed(2)}', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: Colors.red.shade800)),
],
),
),
),
),
],
),
);
}
🚨 8. Alerta de Vencimento e Lista de Pagamentos
A parte inferior da tela contém um Card de alerta urgente para itens vencendo “hoje”, seguido de uma lista agrupada (Pagar/Receber).
Componente de Alerta de Vencimento:
Widget _buildDueTodayAlert(BuildContext context) {
// Simulação de um alerta. Na vida real, filtraria do provedor.
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(Icons.warning_amber_rounded, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vencimento Hoje', style: TextStyle(color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold)),
Text('Fatura Cartão de Crédito - R\$ 1.250,00', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onErrorContainer)),
],
),
),
FilledButton.tonal(
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Colors.white),
onPressed: () {},
child: const Text('PAGAR'),
)
],
),
);
}
🧩 9. Juntando Tudo: A Tela Home Completa
Agora, orquestramos todos esses componentes dentro do nosso ConsumerWidget, escutando o Riverpod para reagir aos dados.
Arquivo: lib/features/home/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/finance_providers.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Escuta as transações do mês
final transactionsAsync = ref.watch(currentMonthTransactionsProvider);
return Scaffold(
appBar: /* AppBar do Passo 5 */,
body: transactionsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Erro: $err')),
data: (transactions) {
return ListView(
children: [
_buildHeaderAndSearch(context), // Passo 6
_buildSummaryCards(context, transactions), // Passo 7
_buildDueTodayAlert(context), // Passo 8
// Título da Lista
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Text('Pagar / Receber', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
),
// Lista de Transações
...transactions.map((t) => ListTile(
leading: CircleAvatar(
backgroundColor: t.isIncome ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
child: Icon(
t.isIncome ? Icons.arrow_upward : Icons.arrow_downward,
color: t.isIncome ? Colors.green : Colors.red,
),
),
title: Text(t.title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('Vence dia ${t.dueDate.day}/${t.dueDate.month}'),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('R\$ ${t.amount.toStringAsFixed(2)}', style: TextStyle(fontWeight: FontWeight.bold, color: t.isIncome ? Colors.green : Colors.black87)),
Text(t.isPaid ? 'Pago' : 'Pendente', style: TextStyle(fontSize: 12, color: t.isPaid ? Colors.green : Colors.orange)),
],
),
)),
const SizedBox(height: 24),
],
);
},
),
);
}
// (Insira os métodos _buildHeaderAndSearch, _buildSummaryCards e _buildDueTodayAlert aqui)
}
Resumo e Próximos Passos
Nesta Parte 1, lançamos as bases de um aplicativo financeiro de nível profissional:
-
Segurança e Velocidade: Modelamos o banco Isar e criamos as rotinas do Riverpod.
-
UX Executiva: O Dashboard (Home) possui a barra de busca, alerta crítico (vencimento do dia), e a inteligência visual de setas e cores (verde/vermelho) para resumir os ganhos e gastos em frações de segundo.
-
Navegação Moderna: Configuramos a M3
NavigationBar