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:
-
Arquitetura Híbrida (Settings): Uma tela para alternar entre Modo Local (Isar) e Modo Nuvem (Supabase).
-
Backup em SQL: Exportar os dados do Isar para um arquivo
.sqlcompatível com bancos relacionais. -
Compartilhamento de Listas: Lógica para permitir que outros usuários editem sua lista.
-
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”:
-
Tabela
lists(id, name, owner_id). -
Tabela
list_members(list_id, user_id). -
Tabela
shopping_itemspassa a terlist_idem vez deuser_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).
-
Configure o projeto no Firebase Console.
-
Adicione
firebase_messagingaopubspec.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:
-
Funciona Offline (Isar) e Online (Supabase).
-
Gerencia estado complexo com BLoC.
-
Possui Interface Moderna e responsiva.
-
Garante a segurança dos dados com Backup SQL.
-
É 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.