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:
-
Configuração do ambiente e dependências.
-
Criação de modelos de dados NoSQL com Isar.
-
Implementação de um serviço reativo usando Streams.
-
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.