Controle de Compras com Flutter: UI Moderna, Integração com APIs e Autenticação (Parte 3)

Neste artigo, vamos elevar o nível do nosso aplicativo. Chega de telas cinzas e dados manuais!

O que vamos construir:

  1. UI/UX de Alto Nível: Cards coloridos, animações fluidas e filtros avançados.

  2. Inteligência de Mercado: Ao escanear um código de barras, o app buscará automaticamente o nome e a foto do produto na internet (usando a API Open Food Facts).

  3. Segurança: Implementação de Autenticação com Firebase para proteger os dados.


📦 Passo 1: Novas Dependências

Vamos adicionar pacotes para requisições HTTP, animações e fontes bonitas.

No seu pubspec.yaml:

dependencies:
  # ... dependências anteriores (isar, bloc, etc)
  
  # HTTP para API
  http: ^1.2.0
  
  # UI e Animações
  flutter_animate: ^4.5.0
  google_fonts: ^6.1.0
  cached_network_image: ^3.3.1 # Para imagens da API
  
  # Autenticação (Firebase)
  firebase_core: ^2.24.2
  firebase_auth: ^4.16.0

Rode flutter pub get.

🌐 Passo 2: Integração com API (Open Food Facts)

Não queremos digitar o nome de todo produto. Vamos criar um serviço que busca os dados automaticamente pelo código de barras.

Crie lib/services/product_lookup_service.dart:

import 'dart:convert';
import 'package:http/http.dart' as http;

class ProductLookupService {
  // API Gratuita e Open Source
  static const String _baseUrl = 'https://world.openfoodfacts.org/api/v0/product';

  Future<Map<String, dynamic>?> lookupProduct(String barcode) async {
    try {
      final response = await http.get(Uri.parse('$_baseUrl/$barcode.json'));

      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        if (data['status'] == 1) {
          final product = data['product'];
          return {
            'name': product['product_name'] ?? '',
            'image_url': product['image_front_small_url'],
            'brands': product['brands'] ?? '',
          };
        }
      }
    } catch (e) {
      print('Erro ao buscar produto: $e');
    }
    return null;
  }
}

Agora, precisamos atualizar nosso BLoC ou a UI para usar isso. Para simplificar, usaremos direto no modal de adicionar produto (passo 4).


🎨 Passo 3: UI Moderna e Colorida

Vamos abandonar o ListTile padrão e criar um Card personalizado, com cores que mudam baseadas na categoria e animações de entrada.

Crie lib/widgets/shopping_item_card.dart:

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../collections/shopping_item.dart';

class ShoppingItemCard extends StatelessWidget {
  final ShoppingItem item;
  final VoidCallback onToggle;
  final VoidCallback onDelete;

  const ShoppingItemCard({
    super.key,
    required this.item,
    required this.onToggle,
    required this.onDelete,
  });

  @override
  Widget build(BuildContext context) {
    // Cor dinâmica baseada se foi comprado ou não
    final bgColor = item.isBought ? Colors.grey.shade200 : Colors.white;
    final textColor = item.isBought ? Colors.grey : Colors.black87;

    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: bgColor,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          if (!item.isBought)
            BoxShadow(
              color: Colors.black.withOpacity(0.05),
              blurRadius: 10,
              offset: const Offset(0, 4),
            ),
        ],
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(16),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onToggle,
            child: Padding(
              padding: const EdgeInsets.all(12.0),
              child: Row(
                children: [
                  // Imagem do Produto (se houver, vinda da API) ou Ícone
                  Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.deepPurple.shade50,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: const Icon(Icons.shopping_bag_outlined, color: Colors.deepPurple),
                  ),
                  const SizedBox(width: 16),
                  
                  // Textos
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          item.name,
                          style: GoogleFonts.poppins(
                            fontSize: 16,
                            fontWeight: FontWeight.w600,
                            decoration: item.isBought ? TextDecoration.lineThrough : null,
                            color: textColor,
                          ),
                        ),
                        if (item.category.value != null)
                          Container(
                            margin: const EdgeInsets.only(top: 4),
                            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                            decoration: BoxDecoration(
                              color: Colors.blue.shade50,
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: Text(
                              item.category.value!.name,
                              style: const TextStyle(fontSize: 10, color: Colors.blue),
                            ),
                          ),
                      ],
                    ),
                  ),

                  // Preço e Qtd
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Text(
                        'R\$ ${(item.price * item.quantity).toStringAsFixed(2)}',
                        style: GoogleFonts.poppins(
                          fontWeight: FontWeight.bold,
                          color: item.isBought ? Colors.grey : Colors.green,
                        ),
                      ),
                      Text('${item.quantity}un', style: TextStyle(fontSize: 12, color: Colors.grey)),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ).animate().fadeIn().slideX(begin: 0.2, end: 0); // Animação de entrada
  }
}

🔐 Passo 4: Autenticação (Firebase Auth)

Para ter listas na nuvem, precisamos saber quem é o usuário. Vamos configurar um login simples.

Nota: Certifique-se de ter configurado o projeto no Console do Firebase e baixado o google-services.json (Android) ou GoogleService-Info.plist (iOS).

Crie lib/services/auth_service.dart:

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Stream<User?> get user => _auth.authStateChanges();

  // Login Simples (Anônimo para teste rápido ou Email/Senha)
  Future<void> signInAnon() async {
    await _auth.signInAnonymously();
  }
  
  Future<void> signOut() async {
    await _auth.signOut();
  }
  
  String? get userId => _auth.currentUser?.uid;
}

Agora, precisamos criar um Wrapper no main.dart para decidir qual tela mostrar.

Atualize lib/main.dart:

import 'package:firebase_core/firebase_core.dart';
// ... outros imports

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(); // Inicializa Firebase
  
  final isarService = IsarService();
  
  runApp(
    BlocProvider(
      create: (context) => ShoppingListBloc(isarService),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ... theme config
      home: const AuthWrapper(), // Decide a tela inicial
    );
  }
}

class AuthWrapper extends StatelessWidget {
  const AuthWrapper({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return const HomeScreen(); // Usuário logado
        }
        return const LoginScreen(); // Usuário deslogado
      },
    );
  }
}

🚀 Passo 5: A Evolução da Home e Filtros Avançados

Vamos juntar a API no momento da adição e criar filtros visuais na Home.

Na HomeScreen (dentro do método _showAddItemDialog):

Modifique a lógica do botão de scanner para chamar a API:

IconButton(
  icon: const Icon(Icons.barcode_reader),
  onPressed: () async {
    // 1. Escaneia
    final scannedBarcode = await Navigator.push<String>(
      context,
      MaterialPageRoute(builder: (c) => const ScannerScreen()),
    );
    
    if (scannedBarcode != null) {
      barcodeController.text = scannedBarcode;
      
      // 2. Busca na API (Loading visual opcional aqui)
      final productData = await ProductLookupService().lookupProduct(scannedBarcode);
      
      if (productData != null) {
        // 3. Preenche automaticamente os campos!
        nameController.text = productData['name'];
        // Se tiver URL de imagem, poderia salvar no banco também
        ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(content: Text('Produto encontrado: ${productData['name']}')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
           const SnackBar(content: Text('Produto não encontrado na base de dados.')),
        );
      }
    }
  },
),

Adicionando Filtros Visuais na Home:

Substitua a lista de Chips simples por algo mais elaborado:

// Widget de Filtros
SizedBox(
  height: 60,
  child: ListView.builder(
    scrollDirection: Axis.horizontal,
    itemCount: state.categories.length + 1,
    itemBuilder: (ctx, i) {
      final isSelected = _selectedCategoryIndex == i; // Você precisará gerenciar esse estado na tela
      return GestureDetector(
        onTap: () {
            setState(() => _selectedCategoryIndex = i);
            // Disparar evento do Bloc para filtrar
        },
        child: AnimatedContainer(
          duration: 300.ms,
          margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
          padding: const EdgeInsets.symmetric(horizontal: 20),
          alignment: Alignment.center,
          decoration: BoxDecoration(
            color: isSelected ? Colors.deepPurple : Colors.white,
            borderRadius: BorderRadius.circular(30),
            border: Border.all(color: Colors.deepPurple.withOpacity(0.2)),
            boxShadow: isSelected ? [
               BoxShadow(color: Colors.deepPurple.withOpacity(0.4), blurRadius: 8, offset: const Offset(0, 4))
            ] : [],
          ),
          child: Text(
            i == 0 ? "Todos" : state.categories[i-1].name,
            style: TextStyle(
              color: isSelected ? Colors.white : Colors.black87,
              fontWeight: FontWeight.bold
            ),
          ),
        ),
      );
    },
  ),
),

✅ Conclusão e Futuro

Nesta Parte 3, transformamos um app funcional em uma experiência completa:

  1. UX Profissional: Usamos flutter_animate e google_fonts para criar cards que dão prazer de usar.

  2. Automação: Integramos a API Open Food Facts, poupando tempo de digitação do usuário, documentação do uso da api (documentacao)

  3. Segurança: Preparamos o terreno com Autenticação.

Para a próxima etapa (Parte 4): Podemos focar em Sincronização na Nuvem com Supabase.

Agora que identificamos o usuário, podemos evoluir nosso armazenamento para a nuvem. Em vez do Firestore, usaremos o Supabase (uma alternativa Open Source poderosa baseada em PostgreSQL).

Isso trará grandes vantagens:

  • Relacionamentos SQL: O Supabase lida muito bem com relações (Categorias <-> Produtos), algo que bancos NoSQL como Firestore dificultam um pouco.

  • Realtime: Assim como o Isar local, o Supabase tem suporte a realtime, permitindo que se você adicionar um item no seu celular, ele apareça instantaneamente no celular da sua esposa/marido.

  • Row Level Security (RLS): Garantiremos que cada usuário veja apenas os seus próprios dados (ou dados compartilhados com a família).

O que acha dessa direção usando PostgreSQL e Supabase? 🚀

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