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:

  1. Segurança e Velocidade: Modelamos o banco Isar e criamos as rotinas do Riverpod.

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

  3. Navegação Moderna: Configuramos a M3 NavigationBar

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