Controle de Compras com Flutter: Backup SQL, Notificações e Modo Híbrido (Parte Final)

Neste artigo final, vamos tornar o aplicativo flexível e colaborativo. Implementaremos uma arquitetura que permite ao usuário escolher onde guardar seus dados, garantiremos que nada se perca com backups e manteremos todos informados com Push Notifications.

O que vamos implementar:

  1. Arquitetura Híbrida (Settings): Uma tela para alternar entre Modo Local (Isar) e Modo Nuvem (Supabase).

  2. Backup em SQL: Exportar os dados do Isar para um arquivo .sql compatível com bancos relacionais.

  3. Compartilhamento de Listas: Lógica para permitir que outros usuários editem sua lista.

  4. Push Notifications: Notificar quando alguém altera a lista.


⚙️ Passo 1: O Poder da Escolha (Settings Screen)

Para permitir a troca de bancos, precisamos de uma Interface Comum. O BLoC não deve saber se é Isar ou Supabase, ele apenas pede dados.

1.1 A Interface (Contrato)

Crie lib/repositories/i_shopping_repository.dart:

abstract class IShoppingRepository {
  Stream<List<ShoppingItem>> getItems();
  Future<void> addItem(ShoppingItem item);
  Future<void> toggleStatus(ShoppingItem item);
  Future<void> deleteItem(int id);
}

Agora, faça suas classes IsarService e SupabaseService implementarem essa interface.

1.2 A Tela de Configurações

Vamos usar o shared_preferences para persistir a escolha do usuário.

Crie lib/screens/settings_screen.dart:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/shopping_list/shopping_list_bloc.dart'; // Seu Bloc
import '../services/backup_service.dart'; // Criaremos no Passo 2

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({super.key});

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  bool _useCloud = false;

  @override
  void initState() {
    super.initState();
    _loadPreferences();
  }

  Future<void> _loadPreferences() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _useCloud = prefs.getBool('use_cloud') ?? false;
    });
  }

  Future<void> _toggleSource(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('use_cloud', value);
    
    setState(() => _useCloud = value);

    // Reinicia o App ou dispara evento para trocar o Repositório no BLoC
    // Exemplo simples: Mostrar aviso para reiniciar
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Reinicie o app para aplicar a troca de banco.')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Configurações')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Usar Sincronização na Nuvem (Supabase)'),
            subtitle: const Text('Desative para usar apenas no dispositivo (Isar)'),
            value: _useCloud,
            onChanged: _toggleSource,
            secondary: Icon(_useCloud ? Icons.cloud : Icons.sd_storage),
          ),
          const Divider(),
          if (!_useCloud) ...[
            ListTile(
              leading: const Icon(Icons.backup),
              title: const Text('Fazer Backup Local'),
              subtitle: const Text('Exportar dados para arquivo .sql'),
              onTap: () async {
                 await BackupService().exportToSql(context);
              },
            ),
          ],
          if (_useCloud) ...[
             ListTile(
              leading: const Icon(Icons.share),
              title: const Text('Gerenciar Compartilhamento'),
              subtitle: const Text('Convide pessoas para sua lista'),
              onTap: () {
                // Navegar para tela de convites
              },
            ),
          ]
        ],
      ),
    );
  }
}

💾 Passo 2: Backup Local em SQL

Como o Isar é NoSQL, os dados não estão em tabelas SQL nativamente. Vamos criar um serviço que lê os objetos e gera um arquivo de texto com comandos INSERT.

Adicione ao pubspec.yaml:

path_provider: ^2.1.2
share_plus: ^7.2.0

Crie lib/services/backup_service.dart:

import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'isar_service.dart'; // Seu serviço local

class BackupService {
  final IsarService _localDb = IsarService();

  Future<void> exportToSql(context) async {
    try {
      // 1. Busca todos os dados do Isar
      final items = await _localDb.getAllItemsOnce(); // Crie este método no IsarService retornando Future<List>
      
      // 2. Constrói a String SQL
      final buffer = StringBuffer();
      buffer.writeln("-- Backup Controle de Compras");
      buffer.writeln("-- Data: ${DateTime.now().toIso8601String()}");
      buffer.writeln("");
      buffer.writeln("CREATE TABLE IF NOT EXISTS shopping_items (id INTEGER, name TEXT, price REAL, quantity INTEGER, is_bought BOOLEAN);");
      buffer.writeln("");
      
      buffer.writeln("INSERT INTO shopping_items (name, price, quantity, is_bought) VALUES");
      
      for (int i = 0; i < items.length; i++) {
        final item = items[i];
        final isLast = i == items.length - 1;
        // Sanitização básica de aspas simples para evitar erro de SQL
        final safeName = item.name.replaceAll("'", "''");
        
        buffer.write("('$safeName', ${item.price}, ${item.quantity}, ${item.isBought ? 1 : 0})");
        buffer.writeln(isLast ? ";" : ",");
      }

      // 3. Salva no arquivo
      final directory = await getApplicationDocumentsDirectory();
      final file = File('${directory.path}/backup_compras.sql');
      await file.writeAsString(buffer.toString());

      // 4. Compartilha o arquivo (WhatsApp, Drive, Email)
      await Share.shareXFiles([XFile(file.path)], text: 'Backup da Lista de Compras');

    } catch (e) {
      print('Erro no backup: $e');
    }
  }
}

🤝 Passo 3: Compartilhamento de Listas (Supabase)

Para compartilhar listas na nuvem, precisamos ajustar nossa lógica de banco de dados no Supabase.

3.1 Modelagem (Conceito)

Em vez de o item ter um user_id direto, criamos o conceito de “Listas”:

  1. Tabela lists (id, name, owner_id).

  2. Tabela list_members (list_id, user_id).

  3. Tabela shopping_items passa a ter list_id em vez de user_id.

3.2 Row Level Security (RLS) Atualizado

A política de segurança no Supabase mudaria para: “Um usuário pode ver itens se o list_id do item estiver na tabela list_members onde o user_id é o meu.”

create policy "Members can see items" on shopping_items
for select using (
  exists (
    select 1 from list_members
    where list_members.list_id = shopping_items.list_id
    and list_members.user_id = auth.uid()
  )
);

No Flutter, basta criar uma tela onde o usuário digita o e-mail de outra pessoa, e você insere esse e-mail (resolvendo para o ID) na tabela list_members.


🔔 Passo 4: Push Notifications

Queremos avisar o usuário: “João adicionou Leite à lista”.

Usaremos o Firebase Cloud Messaging (FCM).

  1. Configure o projeto no Firebase Console.

  2. Adicione firebase_messaging ao pubspec.yaml.

No lib/main.dart:

import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print("Handling a background message: ${message.messageId}");
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  
  // Configura Handler de Background
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(const MyApp());
}

class MyApp extends StatefulWidget { ... }

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    setupPushNotifications();
  }

  void setupPushNotifications() async {
    final fcm = FirebaseMessaging.instance;
    await fcm.requestPermission();
    
    // Pega o Token para enviar para o Backend
    final token = await fcm.getToken();
    print("Device Token: $token");
    
    // Salve este token no Supabase (tabela profiles ou user_tokens)
    // Para que você possa enviar notificações para este usuário específico.

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Recebi uma mensagem com o app aberto!');
      if (message.notification != null) {
        // Mostra um SnackBar ou Atualiza o BLoC
        ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.notification!.body!)));
      }
    });
  }
}

Como disparar? Você pode usar Supabase Edge Functions (Database Webhooks). Quando um INSERT ocorrer na tabela shopping_items, a função do Supabase dispara uma chamada para a API do Firebase enviando a notificação para os membros daquela lista.


✅ Conclusão da Série

Chegamos ao fim da jornada Controle de Compras! 🚀

Você saiu do zero e construiu um aplicativo que:

  1. Funciona Offline (Isar) e Online (Supabase).

  2. Gerencia estado complexo com BLoC.

  3. Possui Interface Moderna e responsiva.

  4. Garante a segurança dos dados com Backup SQL.

  5. É colaborativo e engajador com Notificações.

Este projeto agora é um excelente case para seu portfólio. Ele demonstra domínio sobre persistência de dados, arquitetura limpa e integração de serviços em nuvem.

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