Construindo um App de Controle de Compras com Flutter e Isar: Categorias, BLoC e Leitor de Código de Barras (Parte 2)

Na Parte 1, estabelecemos a base do nosso aplicativo de Controle de Compras com Flutter e o poderoso banco de dados Isar. Agora, é hora de expandir as funcionalidades e aprimorar a arquitetura para torná-lo ainda mais útil e robusto.

Neste artigo, vamos mergulhar nos seguintes tópicos:

  1. Categorias de Produtos: Adicionar uma nova entidade (Category) para organizar os itens (Hortifruti, Biscoitos, etc.).

  2. Cálculo Total do Carrinho: Exibir o valor total dos itens selecionados (ou de todos os itens).

  3. Gerenciamento de Estado com BLoC: Refatorar a lógica da lista de compras para usar o padrão BLoC, garantindo escalabilidade e testabilidade.

  4. Leitor de Código de Barras: Integrar a câmera para adicionar produtos rapidamente via código de barras.

  5. Novas Opções e UI: Pequenos ajustes na interface para acomodar as novas funcionalidades.

Vamos transformar nossa lista de compras simples em um verdadeiro assistente de mercado!


📦 Passo 1: Adicionando Novas Dependências

Para BLoC e o leitor de código de barras, precisaremos de algumas adições no pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  isar_community: ^3.3.0
  isar_community_flutter_libs: ^3.3.0
  path_provider: ^2.1.5
  intl: ^0.20.2
  
  # BLoC para gerenciamento de estado
  flutter_bloc: ^9.1.1
  equatable: ^2.0.8

  # Leitor de Código de Barras
  mobile_scanner: ^5.2.3
  
dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.13
  isar_community_generator: ^3.3.0
  bloc_test: ^10.0.0 # Para testes futuros

Após salvar, rode flutter pub get.


🛠️ Passo 2: Modelagem de Dados – Categorias

Vamos criar uma nova “Collection” no Isar para nossas categorias de produtos.

Crie lib/collections/category.dart:

import 'package:isar/isar.dart';

part 'category.g.dart';

@collection
class Category {
  Id id = Isar.autoIncrement;

  late String name;
  String? description;
  List<String>? imageUrls; // Para imagens representativas da categoria

  Category({required this.name, this.description, this.imageUrls});
}

E vamos ligar cada ShoppingItem a uma Category (opcionalmente, um item pode não ter categoria).

Atualize lib/collections/shopping_item.dart:

import 'package:isar/isar.dart';
import 'category.dart'; // Importe a nova collection

part 'shopping_item.g.dart';

@collection
class ShoppingItem {
  Id id = Isar.autoIncrement;

  late String name;
  late double price;
  int quantity = 1;
  bool isBought = false;
  DateTime createdAt = DateTime.now();
  String? barcode; // Novo campo para o código de barras

  // Relacionamento com Categoria (referência lazy)
  final category = IsarLink<Category>();
}

Importante: Rode dart run build_runner build novamente para que o Isar gere o código para a nova Category e atualize o ShoppingItem.


⚙️ Passo 3: Evoluindo o Serviço Isar

Nosso IsarService precisará de métodos para gerenciar categorias e um método para buscar itens pelo código de barras.

Atualize lib/services/isar_service.dart:

import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import '../collections/shopping_item.dart';
import '../collections/category.dart'; // Nova importação

class IsarService {
  late Future<Isar> db;

  IsarService() {
    db = openDB();
  }

  Future<Isar> openDB() async {
    final dir = await getApplicationDocumentsDirectory();
    if (Isar.instanceNames.isEmpty) {
      return await Isar.open(
        [ShoppingItemSchema, CategorySchema], // Adicione CategorySchema aqui
        directory: dir.path,
      );
    }
    return Future.value(Isar.getInstance());
  }

  // --- Métodos de ShoppingItem (existentes, com pequenas melhorias) ---

  Future<void> saveItem(ShoppingItem item) async {
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.shoppingItems.put(item);
      // Se o item tem categoria, salve a categoria também se for nova
      await item.category.save(); 
    });
  }

  Stream<List<ShoppingItem>> listenToItems({int? categoryId}) async* {
    final isar = await db;
    // Filtra por categoria, se fornecido
    if (categoryId != null) {
      yield* isar.shoppingItems
          .filter()
          .category((q) => q.idEqualTo(categoryId))
          .sortByCreatedAtDesc()
          .watch(fireImmediately: true);
    } else {
      yield* isar.shoppingItems.where().sortByCreatedAtDesc().watch(fireImmediately: true);
    }
  }

  Future<ShoppingItem?> getItemByBarcode(String barcode) async {
    final isar = await db;
    return await isar.shoppingItems.filter().barcodeEqualTo(barcode).findFirst();
  }

  // --- Novos Métodos para Category ---

  Future<void> saveCategory(Category category) async {
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.categorys.put(category);
    });
  }

  Stream<List<Category>> listenToCategories() async* {
    final isar = await db;
    yield* isar.categorys.where().sortByName().watch(fireImmediately: true);
  }

  Future<void> deleteCategory(int id) async {
    final isar = await db;
    await isar.writeTxn(() async {
      // Opcional: Se quiser, adicione lógica para lidar com itens nesta categoria
      await isar.categorys.delete(id);
    });
  }
}

🧠 Passo 4: Gerenciamento de Estado com BLoC

Vamos criar um BLoC para a nossa lista de compras. Ele será responsável por interagir com o IsarService e notificar a UI sobre as mudanças.

4.1 Eventos (shopping_list_event.dart)

part of 'shopping_list_bloc.dart';

abstract class ShoppingListEvent extends Equatable {
  const ShoppingListEvent();
  @override
  List<Object> get props => [];
}

class LoadShoppingList extends ShoppingListEvent {
  final int? categoryId; // Para carregar por categoria
  const LoadShoppingList({this.categoryId});
}

class AddShoppingItem extends ShoppingListEvent {
  final String name;
  final double price;
  final int quantity;
  final String? barcode;
  final Category? category;
  const AddShoppingItem({
    required this.name,
    required this.price,
    required this.quantity,
    this.barcode,
    this.category,
  });
}

class ToggleItemStatus extends ShoppingListEvent {
  final ShoppingItem item;
  const ToggleItemStatus(this.item);
}

class DeleteShoppingItem extends ShoppingListEvent {
  final int itemId;
  const DeleteShoppingItem(this.itemId);
}

// Eventos de Categoria
class LoadCategories extends ShoppingListEvent {}
class AddCategory extends ShoppingListEvent {
  final String name;
  const AddCategory(this.name);
}

4.2 Estados (shopping_list_state.dart)

part of 'shopping_list_bloc.dart';

abstract class ShoppingListState extends Equatable {
  const ShoppingListState();
  @override
  List<Object> get props => [];
}

class ShoppingListLoading extends ShoppingListState {}

class ShoppingListLoaded extends ShoppingListState {
  final List<ShoppingItem> items;
  final List<Category> categories;
  final double totalValue;
  final double totalBoughtValue;

  const ShoppingListLoaded({
    this.items = const [],
    this.categories = const [],
    this.totalValue = 0.0,
    this.totalBoughtValue = 0.0,
  });

  ShoppingListLoaded copyWith({
    List<ShoppingItem>? items,
    List<Category>? categories,
    double? totalValue,
    double? totalBoughtValue,
  }) {
    return ShoppingListLoaded(
      items: items ?? this.items,
      categories: categories ?? this.categories,
      totalValue: totalValue ?? this.totalValue,
      totalBoughtValue: totalBoughtValue ?? this.totalBoughtValue,
    );
  }

  @override
  List<Object> get props => [items, categories, totalValue, totalBoughtValue];
}

class ShoppingListError extends ShoppingListState {
  final String message;
  const ShoppingListError(this.message);
}

4.3 O BLoC (shopping_list_bloc.dart)

import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../collections/shopping_item.dart';
import '../collections/category.dart';
import '../services/isar_service.dart';

part 'shopping_list_event.dart';
part 'shopping_list_state.dart';

class ShoppingListBloc extends Bloc<ShoppingListEvent, ShoppingListState> {
  final IsarService _isarService;
  StreamSubscription? _itemsSubscription;
  StreamSubscription? _categoriesSubscription;

  ShoppingListBloc(this._isarService) : super(ShoppingListLoading()) {
    on<LoadShoppingList>(_onLoadShoppingList);
    on<AddShoppingItem>(_onAddShoppingItem);
    on<ToggleItemStatus>(_onToggleItemStatus);
    on<DeleteShoppingItem>(_onDeleteShoppingItem);
    on<LoadCategories>(_onLoadCategories);
    on<AddCategory>(_onAddCategory);

    // Começa a ouvir os streams do Isar assim que o BLoC é criado
    _itemsSubscription = _isarService.listenToItems().listen((items) {
      _updateStateWithItems(items);
    });
    _categoriesSubscription = _isarService.listenToCategories().listen((categories) {
      _updateStateWithCategories(categories);
    });
  }

  // Descalcula os totais
  void _updateStateWithItems(List<ShoppingItem> items) {
    final total = items.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
    final totalBought = items.where((item) => item.isBought).fold(0.0, (sum, item) => sum + (item.price * item.quantity));

    if (state is ShoppingListLoaded) {
      emit((state as ShoppingListLoaded).copyWith(
        items: items,
        totalValue: total,
        totalBoughtValue: totalBought,
      ));
    } else {
      emit(ShoppingListLoaded(
        items: items,
        totalValue: total,
        totalBoughtValue: totalBought,
        categories: (state is ShoppingListLoaded) ? (state as ShoppingListLoaded).categories : [],
      ));
    }
  }

  void _updateStateWithCategories(List<Category> categories) {
    if (state is ShoppingListLoaded) {
      emit((state as ShoppingListLoaded).copyWith(categories: categories));
    } else {
      emit(ShoppingListLoaded(categories: categories));
    }
  }

  Future<void> _onLoadShoppingList(LoadShoppingList event, Emitter<ShoppingListState> emit) async {
    // A lógica de carregamento inicial já está nos streams, aqui podemos apenas emitir o estado atual
    // Ou re-emitir um loading para "forçar" um refresh visual
    emit(ShoppingListLoading());
    // Os listeners já vão atualizar o estado para Loaded
  }

  Future<void> _onAddShoppingItem(AddShoppingItem event, Emitter<ShoppingListState> emit) async {
    try {
      final newItem = ShoppingItem()
        ..name = event.name
        ..price = event.price
        ..quantity = event.quantity
        ..barcode = event.barcode;
      
      // Ligar o item à categoria, se houver
      if (event.category != null) {
        newItem.category.value = event.category;
      }
      
      await _isarService.saveItem(newItem);
    } catch (e) {
      emit(ShoppingListError("Falha ao adicionar item: $e"));
    }
  }

  Future<void> _onToggleItemStatus(ToggleItemStatus event, Emitter<ShoppingListState> emit) async {
    try {
      await _isarService.toggleStatus(event.item);
    } catch (e) {
      emit(ShoppingListError("Falha ao atualizar status: $e"));
    }
  }

  Future<void> _onDeleteShoppingItem(DeleteShoppingItem event, Emitter<ShoppingListState> emit) async {
    try {
      await _isarService.deleteItem(event.itemId);
    } catch (e) {
      emit(ShoppingListError("Falha ao deletar item: $e"));
    }
  }

  Future<void> _onLoadCategories(LoadCategories event, Emitter<ShoppingListState> emit) async {
    // Já está sendo ouvido via stream, mas pode ser usado para um refresh manual
  }

  Future<void> _onAddCategory(AddCategory event, Emitter<ShoppingListState> emit) async {
    try {
      final newCategory = Category(name: event.name);
      await _isarService.saveCategory(newCategory);
    } catch (e) {
      emit(ShoppingListError("Falha ao adicionar categoria: $e"));
    }
  }

  @override
  Future<void> close() {
    _itemsSubscription?.cancel();
    _categoriesSubscription?.cancel();
    return super.close();
  }
}

📱 Passo 5: Integrando a UI com BLoC e Novas Funcionalidades

Nosso HomeScreen será refatorado para usar o BLoC e adicionar as novas opções.

5.1 main.dart (Injetando o BLoC)

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'screens/home_screen.dart';
import 'services/isar_service.dart';
import 'blocs/shopping_list/shopping_list_bloc.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized(); // Garante que o Flutter esteja inicializado antes de usar o Isar  
  
  final isarService = IsarService(); // Cria uma única instância do serviço
  runApp(
    BlocProvider(
      create: (context) => ShoppingListBloc(isarService), // Injeta o BLoC
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Controle de Compras',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

5.2 home_screen.dart (Consumindo o BLoC, Categorias e Totais)

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../blocs/shopping_list/shopping_list_bloc.dart';
import '../collections/shopping_item.dart';
import '../collections/category.dart';
import 'scanner_screen.dart'; // Criaremos esta tela

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final NumberFormat currencyFormatter = NumberFormat.simpleCurrency(locale: 'pt_BR');

  @override
  void initState() {
    super.initState();
    // Dispara o evento inicial para carregar dados
    context.read<ShoppingListBloc>().add(LoadShoppingList());
    context.read<ShoppingListBloc>().add(LoadCategories());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Minha Lista de Compras'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.category),
            onPressed: () => _showManageCategoriesDialog(context),
            tooltip: 'Gerenciar Categorias',
          ),
        ],
      ),
      body: BlocBuilder<ShoppingListBloc, ShoppingListState>(
        builder: (context, state) {
          if (state is ShoppingListLoading) {
            return const Center(child: CircularProgressIndicator());
          }
          if (state is ShoppingListError) {
            return Center(child: Text('Erro: ${state.message}'));
          }
          if (state is ShoppingListLoaded) {
            return Column(
              children: [
                // Totais do Carrinho
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Card(
                    elevation: 4,
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        children: [
                          _buildTotalColumn('Total Geral', state.totalValue, Colors.green),
                          _buildTotalColumn('Total Comprado', state.totalBoughtValue, Colors.blue),
                        ],
                      ),
                    ),
                  ),
                ),
                // Categorias (Horizontal Scroll)
                if (state.categories.isNotEmpty)
                  SizedBox(
                    height: 50,
                    child: ListView.builder(
                      scrollDirection: Axis.horizontal,
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
                      itemCount: state.categories.length + 1, // +1 para "Todos"
                      itemBuilder: (ctx, i) {
                        if (i == 0) {
                          return _buildCategoryChip('Todos', null);
                        }
                        final category = state.categories[i - 1];
                        return _buildCategoryChip(category.name, category.id);
                      },
                    ),
                  ),
                const Divider(),
                // Lista de Itens
                Expanded(
                  child: state.items.isEmpty
                      ? const Center(child: Text('Nenhum item na lista.'))
                      : ListView.separated(
                          itemCount: state.items.length,
                          separatorBuilder: (ctx, i) => const Divider(indent: 16, endIndent: 16),
                          itemBuilder: (context, index) {
                            final item = state.items[index];
                            return ListTile(
                              leading: Checkbox(
                                value: item.isBought,
                                onChanged: (bool? value) {
                                  context.read<ShoppingListBloc>().add(ToggleItemStatus(item));
                                },
                              ),
                              title: Text(
                                item.name,
                                style: TextStyle(
                                  decoration: item.isBought ? TextDecoration.lineThrough : null,
                                  color: item.isBought ? Colors.grey : Colors.black,
                                ),
                              ),
                              subtitle: Text(
                                '${item.quantity}x - ${currencyFormatter.format(item.price)} '
                                '${item.category.value != null ? '(${item.category.value!.name})' : ''}',
                              ),
                              trailing: IconButton(
                                icon: const Icon(Icons.delete, color: Colors.redAccent),
                                onPressed: () {
                                  context.read<ShoppingListBloc>().add(DeleteShoppingItem(item.id!));
                                },
                              ),
                            );
                          },
                        ),
                ),
              ],
            );
          }
          return const SizedBox.shrink();
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddItemDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildTotalColumn(String title, double value, Color color) {
    return Column(
      children: [
        Text(title, style: TextStyle(fontSize: 14, color: Colors.grey.shade700)),
        Text(
          currencyFormatter.format(value),
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color),
        ),
      ],
    );
  }

  Widget _buildCategoryChip(String name, int? categoryId) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 4.0),
      child: ChoiceChip(
        label: Text(name),
        selected: false, // Lógica de seleção futura se quiser filtrar
        onSelected: (selected) {
          // TODO: Implementar filtro por categoria no BLoC
          debugPrint('Filtrar por $name (ID: $categoryId)');
        },
      ),
    );
  }

  // --- Diálogos e Telas Auxiliares ---

  Future<void> _showAddItemDialog(BuildContext context) async {
    final nameController = TextEditingController();
    final priceController = TextEditingController();
    final qtdController = TextEditingController(text: '1');
    final barcodeController = TextEditingController();
    Category? selectedCategory;

    await showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) => BlocBuilder<ShoppingListBloc, ShoppingListState>(
        builder: (context, state) {
          final categories = (state is ShoppingListLoaded) ? state.categories : <Category>[];
          return Padding(
            padding: EdgeInsets.only(
              bottom: MediaQuery.of(context).viewInsets.bottom + 20,
              top: 20,
              left: 20,
              right: 20,
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text('Novo Item', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                const SizedBox(height: 10),
                TextField(
                  controller: nameController,
                  decoration: const InputDecoration(labelText: 'Nome do Produto'),
                ),
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: priceController,
                        decoration: const InputDecoration(labelText: 'Preço (R\$)'),
                        keyboardType: TextInputType.number,
                      ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      child: TextField(
                        controller: qtdController,
                        decoration: const InputDecoration(labelText: 'Quantidade'),
                        keyboardType: TextInputType.number,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 10),
                // Campo de Código de Barras com botão de scanner
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: barcodeController,
                        decoration: const InputDecoration(labelText: 'Código de Barras'),
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.barcode_reader),
                      onPressed: () async {
                        final scannedBarcode = await Navigator.push<String>(
                          context,
                          MaterialPageRoute(builder: (c) => const ScannerScreen()),
                        );
                        if (scannedBarcode != null) {
                          barcodeController.text = scannedBarcode;
                          // Opcional: buscar produto em uma API/DB por este código
                        }
                      },
                    ),
                  ],
                ),
                const SizedBox(height: 10),
                // Seletor de Categoria
                DropdownButtonFormField<Category>(
                  value: selectedCategory,
                  hint: const Text('Selecionar Categoria'),
                  items: categories.map((cat) => DropdownMenuItem(value: cat, child: Text(cat.name))).toList(),
                  onChanged: (cat) => selectedCategory = cat,
                ),
                const SizedBox(height: 20),
                FilledButton(
                  onPressed: () {
                    if (nameController.text.isNotEmpty) {
                      context.read<ShoppingListBloc>().add(
                            AddShoppingItem(
                              name: nameController.text,
                              price: double.tryParse(priceController.text.replaceAll(',', '.')) ?? 0.0,
                              quantity: int.tryParse(qtdController.text) ?? 1,
                              barcode: barcodeController.text.isNotEmpty ? barcodeController.text : null,
                              category: selectedCategory,
                            ),
                          );
                      Navigator.pop(context);
                    }
                  },
                  child: const Text('ADICIONAR'),
                )
              ],
            ),
          );
        },
      ),
    );
  }

  void _showManageCategoriesDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Gerenciar Categorias'),
        content: BlocBuilder<ShoppingListBloc, ShoppingListState>(
          builder: (context, state) {
            if (state is! ShoppingListLoaded) return const CircularProgressIndicator();
            final categories = state.categories;
            return SizedBox(
              width: double.maxFinite,
              child: ListView.builder(
                shrinkWrap: true,
                itemCount: categories.length,
                itemBuilder: (context, index) {
                  final category = categories[index];
                  return ListTile(
                    title: Text(category.name),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete, color: Colors.redAccent),
                      onPressed: () {
                        context.read<ShoppingListBloc>().add(DeleteCategory(category.id!));
                      },
                    ),
                  );
                },
              ),
            );
          },
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('Fechar'),
          ),
          FilledButton(
            onPressed: () => _showAddCategoryDialog(context),
            child: const Text('Adicionar Nova'),
          ),
        ],
      ),
    );
  }

  void _showAddCategoryDialog(BuildContext context) {
    final categoryNameController = TextEditingController();
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Adicionar Categoria'),
        content: TextField(
          controller: categoryNameController,
          decoration: const InputDecoration(labelText: 'Nome da Categoria'),
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
          FilledButton(
            onPressed: () {
              if (categoryNameController.text.isNotEmpty) {
                context.read<ShoppingListBloc>().add(AddCategory(categoryNameController.text));
                Navigator.pop(ctx); // Fecha o dialog de adicionar categoria
                Navigator.pop(ctx); // Fecha o dialog de gerenciar categorias
              }
            },
            child: const Text('Salvar'),
          ),
        ],
      ),
    );
  }
}

📸 Passo 6: Tela de Leitor de Código de Barras

A tela ScannerScreen que usamos no CarControl pode ser reutilizada aqui.

Crie lib/screens/scanner_screen.dart:

import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Escanear Código de Barras')),
      body: MobileScanner(
        onDetect: (capture) {
          final List<Barcode> barcodes = capture.barcodes;
          if (barcodes.isNotEmpty && barcodes.first.rawValue != null) {
            final String code = barcodes.first.rawValue!;
            Navigator.pop(context, code); // Retorna o código lido
          }
        },
      ),
    );
  }
}

Importante: Não se esqueça de adicionar as permissões de câmera no seu AndroidManifest.xml (Android) e Info.plist (iOS) para o mobile_scanner funcionar!

✅ Conclusão

Com esta segunda parte, transformamos completamente o nosso app de Controle de Compras! Agora ele possui:

  1. Organização por Categorias: Gerencie seus produtos de forma mais estruturada.

  2. BLoC para Gerenciamento de Estado: Uma arquitetura robusta e escalável para lidar com as interações do usuário e atualizações do banco de dados.

  3. Cálculo de Totais: Tenha uma visão clara do custo da sua compra.

  4. Leitor de Código de Barras: Agilize a adição de produtos à lista.

Nos próximos artigos, podemos explorar:

  • Autenticação de Usuários: Para listas de compras compartilhadas ou persistência na nuvem.

  • Melhorias na UI/UX: Animações, filtros mais avançados, e designs mais elaborados.

  • Integração com APIs: Para buscar informações de produtos automaticamente.

Continue acompanhando para construir aplicações Flutter cada vez mais complexas e profissionais! 🚀🛒

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