Construindo um App de Controle de Veículos: UI Profissional, Validação e Reviews (Parte 3)

Até agora, nosso app aceitava qualquer dado e a interface era básica. Vamos mudar isso adicionando regras de negócio, validações visuais e um sistema de relacionamento entre tabelas (Carros e Avaliações).


🛠️ Passo 1: Evoluindo o Banco de Dados (Reviews)

Para criar o sistema de reviews, precisamos de uma nova tabela que se relacione com a tabela carros.

Atualize o lib/data/db_helper.dart:

Vamos alterar a criação do banco para incluir a tabela reviews e adicionar uma função de busca.

Nota: Se você já rodou o app antes, desinstale-o do emulador para resetar o banco de dados, pois estamos mudando a estrutura (onCreate só roda uma vez).

// ... imports anteriores

class DBHelper {
  // ... código singleton anterior ...

  Future<Database> _initDB() async {
    String path = join(await getDatabasesPath(), 'car_control.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        // Tabela de Carros
        await db.execute('''
          CREATE TABLE carros(
            id TEXT PRIMARY KEY,
            modelo TEXT,
            marca TEXT,
            ano INTEGER,
            placa TEXT
          )
        ''');
        
        // Tabela de Reviews (Relacionamento 1:N)
        await db.execute('''
          CREATE TABLE reviews(
            id TEXT PRIMARY KEY,
            carroId TEXT,
            nota INTEGER,
            comentario TEXT,
            data TEXT,
            FOREIGN KEY(carroId) REFERENCES carros(id) ON DELETE CASCADE
          )
        ''');
      },
    );
  }

  // --- NOVOS MÉTODOS ---

  // Buscar carros pelo nome (Search)
  Future<List<Carro>> searchCarros(String query) async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(
      'carros',
      where: 'modelo LIKE ? OR marca LIKE ?',
      whereArgs: ['%$query%', '%$query%'],
    );
    return List.generate(maps.length, (i) => Carro.fromMap(maps[i]));
  }

  // Inserir Review
  Future<void> insertReview(String carroId, int nota, String comentario) async {
    final db = await database;
    await db.insert('reviews', {
      'id': DateTime.now().toIso8601String(),
      'carroId': carroId,
      'nota': nota,
      'comentario': comentario,
      'data': DateTime.now().toString(),
    });
  }

  // Pegar Reviews de um Carro
  Future<List<Map<String, dynamic>>> getReviews(String carroId) async {
    final db = await database;
    return await db.query('reviews', where: 'carroId = ?', whereArgs: [carroId]);
  }
}

🎨 Passo 2: Formulário com Validação (Refinamento de UI)

Vamos criar a tela AddCarroScreen usando TextFormField e GlobalKey para validar se o usuário digitou tudo corretamente.

Crie/Atualize lib/screens/add_carro_screen.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import '../providers/carro_provider.dart';

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

  @override
  State<AddCarroScreen> createState() => _AddCarroScreenState();
}

class _AddCarroScreenState extends State<AddCarroScreen> {
  final _formKey = GlobalKey<FormState>();
  
  // Controllers para capturar o texto
  final _modeloController = TextEditingController();
  final _marcaController = TextEditingController();
  final _anoController = TextEditingController();
  final _placaController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Novo Veículo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form( // Widget que habilita validação
          key: _formKey,
          child: ListView(
            children: [
              _buildInput(
                controller: _modeloController,
                label: 'Modelo',
                icon: Icons.directions_car,
                validator: (value) => value!.isEmpty ? 'Informe o modelo' : null,
              ),
              const SizedBox(height: 16),
              _buildInput(
                controller: _marcaController,
                label: 'Marca',
                icon: Icons.branding_watermark,
                validator: (value) => value!.isEmpty ? 'Informe a marca' : null,
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: _buildInput(
                      controller: _anoController,
                      label: 'Ano',
                      icon: Icons.calendar_today,
                      keyboardType: TextInputType.number,
                      validator: (value) {
                        if (value!.isEmpty) return 'Informe o ano';
                        if (int.tryParse(value) == null) return 'Inválido';
                        return null;
                      },
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: _buildInput(
                      controller: _placaController,
                      label: 'Placa',
                      icon: Icons.tag,
                      validator: (value) => value!.length < 7 ? 'Placa incompleta' : null,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 32),
              FilledButton.icon( // Botão moderno do Material 3
                onPressed: _saveCarro,
                icon: const Icon(Icons.save),
                label: const Text('SALVAR VEÍCULO'),
                style: FilledButton.styleFrom(padding: const EdgeInsets.all(16)),
              ),
            ],
          ),
        ),
      ),
    );
  }

  // Método auxiliar para criar inputs bonitos
  Widget _buildInput({
    required TextEditingController controller,
    required String label,
    required IconData icon,
    String? Function(String?)? validator,
    TextInputType keyboardType = TextInputType.text,
  }) {
    return TextFormField(
      controller: controller,
      decoration: InputDecoration(
        labelText: label,
        prefixIcon: Icon(icon),
        border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
        filled: true,
        fillColor: Colors.grey.shade100,
      ),
      keyboardType: keyboardType,
      validator: validator,
    );
  }

  void _saveCarro() {
    if (_formKey.currentState!.validate()) {
      Provider.of<CarroProvider>(context, listen: false).addCarro(
        _modeloController.text,
        _marcaController.text,
        int.parse(_anoController.text),
        _placaController.text,
      );
      context.pop(); // Volta para a Home
    }
  }
}

🔍 Passo 3: Tela de Detalhes e Reviews

Esta tela mostrará os detalhes do carro e uma lista de avaliações. Usaremos um Modal para adicionar novas avaliações sem sair da tela.

Crie lib/screens/car_detail_screen.dart:

import 'package:flutter/material.dart';
import '../models/carro.dart';
import '../data/db_helper.dart';

class CarDetailScreen extends StatefulWidget {
  final Carro carro;
  const CarDetailScreen({super.key, required this.carro});

  @override
  State<CarDetailScreen> createState() => _CarDetailScreenState();
}

class _CarDetailScreenState extends State<CarDetailScreen> {
  List<Map<String, dynamic>> _reviews = [];

  @override
  void initState() {
    super.initState();
    _loadReviews();
  }

  Future<void> _loadReviews() async {
    final reviews = await DBHelper().getReviews(widget.carro.id);
    setState(() => _reviews = reviews);
  }

  void _addReviewDialog() {
    final comentarioController = TextEditingController();
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Avaliar Carro'),
        content: TextField(
          controller: comentarioController,
          decoration: const InputDecoration(hintText: 'Digite seu comentário...'),
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
          FilledButton(
            onPressed: () async {
              await DBHelper().insertReview(widget.carro.id, 5, comentarioController.text);
              _loadReviews(); // Recarrega a lista
              if (mounted) Navigator.pop(ctx);
            },
            child: const Text('Avaliar'),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.carro.modelo)),
      body: Column(
        children: [
          // Cabeçalho do Carro
          Container(
            padding: const EdgeInsets.all(24),
            width: double.infinity,
            color: Theme.of(context).colorScheme.primaryContainer,
            child: Column(
              children: [
                const Icon(Icons.directions_car, size: 80),
                Text(widget.carro.marca, style: Theme.of(context).textTheme.headlineSmall),
                Text('Ano: ${widget.carro.ano} • Placa: ${widget.carro.placa}'),
              ],
            ),
          ),
          
          // Lista de Reviews
          Expanded(
            child: _reviews.isEmpty
                ? const Center(child: Text('Nenhuma avaliação ainda.'))
                : ListView.builder(
                    itemCount: _reviews.length,
                    itemBuilder: (ctx, i) {
                      final r = _reviews[i];
                      return ListTile(
                        leading: const CircleAvatar(child: Icon(Icons.star, size: 16)),
                        title: Text(r['comentario']),
                        subtitle: Text('Nota: ${r['nota']}'),
                      );
                    },
                  ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _addReviewDialog,
        label: const Text('Avaliar'),
        icon: const Icon(Icons.rate_review),
      ),
    );
  }
}

🔎 Passo 4: Tela de Busca (Search)

Vamos usar o método showSearch nativo do Flutter, que fornece uma UX excelente para buscas.

Crie lib/delegates/carro_search_delegate.dart:

import 'package:flutter/material.dart';
import '../data/db_helper.dart';
import '../models/carro.dart';
import '../screens/car_detail_screen.dart';

class CarroSearchDelegate extends SearchDelegate {
  @override
  List<Widget>? buildActions(BuildContext context) {
    return [IconButton(icon: const Icon(Icons.clear), onPressed: () => query = '')];
  }

  @override
  Widget? buildLeading(BuildContext context) {
    return IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => close(context, null));
  }

  @override
  Widget buildResults(BuildContext context) {
    return FutureBuilder<List<Carro>>(
      future: DBHelper().searchCarros(query),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
        final results = snapshot.data!;
        
        return ListView.builder(
          itemCount: results.length,
          itemBuilder: (ctx, i) {
            final carro = results[i];
            return ListTile(
              title: Text(carro.modelo),
              subtitle: Text(carro.marca),
              onTap: () {
                Navigator.push(context, MaterialPageRoute(
                  builder: (_) => CarDetailScreen(carro: carro),
                ));
              },
            );
          },
        );
      },
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) => const Center(child: Text('Digite para buscar...'));
}

Para ativar a busca, adicione um botão na HomeScreen (dentro do AppBar):

actions: [
  IconButton(
    icon: const Icon(Icons.search),
    onPressed: () => showSearch(context: context, delegate: CarroSearchDelegate()),
  )
],

🌊 Bônus: Splash Screen Nativa

Para uma Splash Screen profissional, não criamos uma tela Flutter, usamos a tela nativa do Android/iOS.

  1. Adicione no pubspec.yaml:

    dev_dependencies:
      flutter_native_splash: ^2.3.10

     

  2. Configure no mesmo arquivo (no final):
    flutter_native_splash:
      color: "#42a5f5"
      image: assets/logo.png # Coloque uma imagem na pasta assets
  3. Rode no terminal:
    dart run flutter_native_splash:create

     

✅ Conclusão

Agora seu app CarControl não é apenas funcional, ele é robusto! Temos:

  • [x] Formulários seguros que impedem dados inválidos.
  • [x] Relacionamento de dados (Reviews vinculadas a Carros).
  • [x] Interface de busca nativa e rápida.
  • [x] Splash screen profissional.

No próximo (e último) artigo, falaremos sobre Arquitetura Limpa (Clean Architecture) e como preparar esse app para produção com testes unitários! 🚀

 

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