Construindo um App de Controle de Compras com Flutter e Isar: O Guia Inicial (Parte 1)

Este artigo irá mostrar como criar um projeto voltado a Controle de Compras, usando o Flutter como Framework e sua linguagem Dart. A partir disso, irei demonstrar os primeiros passos para a criação das primeiras telas e o uso do banco de dados Isar.

Gerenciar compras de supermercado ou listas de desejos é um caso de uso clássico para aplicações móveis. Precisamos de persistência de dados rápida (offline-first), uma interface reativa e facilidade de uso. É aqui que o Isar Database brilha: ele é um banco NoSQL extremamente rápido, feito sob medida para o Flutter.

Vamos colocar a mão na massa!

🚀 Passo 1: Configuração do Projeto

Primeiro, vamos criar o projeto e organizar a estrutura de pastas para que o aplicativo cresça de forma saudável.

No seu terminal, execute:

flutter create shopping_control
cd shopping_control

Dentro da pasta lib, vamos adotar uma estrutura simples, mas organizada:

lib/
├── collections/    # Modelos do banco de dados (Isar)
├── screens/        # Telas do aplicativo
├── services/       # Lógica de acesso ao banco
└── main.dart       # Ponto de entrada

📦 Passo 2: Adicionando Dependências

O Isar precisa de alguns pacotes para funcionar e gerar código automaticamente. Abra o arquivo pubspec.yaml e adicione:

dependencies:
  flutter:
    sdk: flutter
  isar: ^3.1.0
  isar_flutter: ^3.1.0
  path_provider: ^2.1.2 # Para encontrar onde salvar o banco no celular
  intl: ^0.19.0         # Para formatar preços (R$)

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.8  # Gerador de código
  isar_generator: ^3.1.0

Após salvar, rode o comando: flutter pub get.

💾 Passo 3: Modelagem do Banco de Dados

Vamos criar a nossa entidade de “Item de Compra”. Crie o arquivo lib/collections/shopping_item.dart.

No Isar, usamos a anotação @collection.

import 'package:isar/isar.dart';

// Esta linha é necessária para o gerador de código
part 'shopping_item.g.dart';

@collection
class ShoppingItem {
  Id id = Isar.autoIncrement; // O Isar gerencia o ID automaticamente

  late String name;
  
  late double price;
  
  int quantity = 1;
  
  bool isBought = false; // Checkbox de "comprado"

  DateTime createdAt = DateTime.now();
}

Agora, o passo mágico. Execute o comando abaixo no terminal para gerar o arquivo .g.dart (a cola que faz o banco funcionar):

dart run build_runner build

🛠️ Passo 4: O Serviço de Banco de Dados

Vamos criar uma classe para isolar as operações do banco. Crie lib/services/isar_service.dart.

Isso permite que a gente abra o banco apenas uma vez e faça operações de CRUD (Criar, Ler, Atualizar, Deletar).

import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import '../collections/shopping_item.dart';

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], // Schema gerado automaticamente
        directory: dir.path,
      );
    }
    return Future.value(Isar.getInstance());
  }

  // 1. Salvar ou Atualizar Item
  Future<void> saveItem(ShoppingItem item) async {
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.shoppingItems.put(item);
    });
  }

  // 2. Ler todos os itens (Stream permite atualização em tempo real)
  Stream<List<ShoppingItem>> listenToItems() async* {
    final isar = await db;
    // Retorna os itens e fica "ouvindo" mudanças no banco
    yield* isar.shoppingItems.where().sortByCreatedAtDesc().watch(fireImmediately: true);
  }

  // 3. Marcar como comprado/não comprado
  Future<void> toggleStatus(ShoppingItem item) async {
    final isar = await db;
    item.isBought = !item.isBought;
    await isar.writeTxn(() async {
      await isar.shoppingItems.put(item);
    });
  }

  // 4. Deletar item
  Future<void> deleteItem(int id) async {
    final isar = await db;
    await isar.writeTxn(() async {
      await isar.shoppingItems.delete(id);
    });
  }
}

📱 Passo 5: Criando a Interface (UI)

Vamos configurar o main.dart e criar nossa tela principal em lib/screens/home_screen.dart.

Em lib/main.dart:

import 'package:flutter/material.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(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,
    );
  }
}

Em lib/screens/home_screen.dart:

Aqui usaremos um StreamBuilder. O Isar tem uma feature incrível chamada .watch(). Sempre que o banco muda, ele avisa a tela para se redesenhar automaticamente, sem precisarmos de setState manual para atualizar a lista.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // Para formatar moeda
import '../services/isar_service.dart';
import '../collections/shopping_item.dart';

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

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

class _HomeScreenState extends State<HomeScreen> {
  final IsarService service = IsarService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Minha Lista de Compras'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: StreamBuilder<List<ShoppingItem>>(
        stream: service.listenToItems(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          
          final items = snapshot.data ?? [];

          if (items.isEmpty) {
            return const Center(child: Text('Nenhum item na lista.'));
          }

          return ListView.separated(
            itemCount: items.length,
            separatorBuilder: (ctx, i) => const Divider(),
            itemBuilder: (context, index) {
              final item = items[index];
              return ListTile(
                leading: Checkbox(
                  value: item.isBought,
                  onChanged: (bool? value) {
                    service.toggleStatus(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 - ${NumberFormat.simpleCurrency(locale: 'pt_BR').format(item.price)}',
                ),
                trailing: IconButton(
                  icon: const Icon(Icons.delete, color: Colors.redAccent),
                  onPressed: () => service.deleteItem(item.id),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddItemDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  // Pequeno modal para adicionar itens
  void _showAddItemDialog(BuildContext context) {
    final nameController = TextEditingController();
    final priceController = TextEditingController();
    final qtdController = TextEditingController(text: '1');

    showModalBottomSheet(
      context: context,
      isScrollControlled: true, // Para o teclado não cobrir
      builder: (context) => 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: 20),
            FilledButton(
              onPressed: () {
                if (nameController.text.isNotEmpty) {
                  final newItem = ShoppingItem()
                    ..name = nameController.text
                    ..price = double.tryParse(priceController.text.replaceAll(',', '.')) ?? 0.0
                    ..quantity = int.tryParse(qtdController.text) ?? 1;
                  
                  service.saveItem(newItem);
                  Navigator.pop(context);
                }
              },
              child: const Text('ADICIONAR'),
            )
          ],
        ),
      ),
    );
  }
}

✅ Conclusão

Parabéns! Você acabou de criar a espinha dorsal de um aplicativo de Controle de Compras.

Neste artigo inicial, cobrimos:

  1. Configuração do ambiente e dependências.

  2. Criação de modelos de dados NoSQL com Isar.

  3. Implementação de um serviço reativo usando Streams.

  4. Criação de uma UI funcional para listar, adicionar, riscar e deletar itens.

Nos próximos passos, podemos evoluir este projeto adicionando categorias (Hortifruti, Limpeza), cálculo total do carrinho e separação da lógica de estado usando BLoC ou Provider.

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