Chat about this codebase

AI-powered code exploration

Online

Project Overview

Kokoshop is a Flutter-based sample application demonstrating end-to-end CRUD operations across four core domains: users, products, shopping cart and sales. It provides a clean, Material-styled interface and basic navigation scaffold to help developers learn Flutter fundamentals or kickstart their own retail-style apps.

Main Capabilities

  • User Management: Create, read, update and delete user profiles
  • Product Catalog: Maintain product listings with full CRUD support
  • Shopping Cart: Add, remove and update cart items; persists in local state
  • Sales Tracking: Record and view completed sales transactions

When to Use Kokoshop

  • As a learning project for Flutter navigation, layouts and state handling
  • As a starter template for retail-style mobile apps
  • For prototyping basic UI flows involving lists, forms and navigation
  • To demo Flutter CRUD patterns in workshops or tutorials

Key Components

main.dart: App Entry Point

Initializes the Flutter app, applies a Material theme and routes to HomeScreen.

import 'package:flutter/material.dart';
import 'home_screen.dart';  // Main menu navigation

void main() {
  runApp(KokoshopApp());
}

class KokoshopApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Kokoshop',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: HomeScreen(),
    );
  }
}

home_screen.dart: Main Menu

Presents navigation buttons for each management section. Developers extend this pattern to hook in additional screens.

import 'package:flutter/material.dart';
import 'user_screen.dart';
import 'product_screen.dart';
import 'cart_screen.dart';
import 'sales_screen.dart';

class HomeScreen extends StatelessWidget {
  Widget _navButton(BuildContext ctx, String label, Widget screen) {
    return ElevatedButton(
      onPressed: () => Navigator.push(ctx, MaterialPageRoute(builder: (_) => screen)),
      child: Text(label),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Kokoshop Dashboard')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _navButton(context, 'Manage Users', UserScreen()),
            _navButton(context, 'Manage Products', ProductScreen()),
            _navButton(context, 'View Cart', CartScreen()),
            _navButton(context, 'View Sales', SalesScreen()),
          ],
        ),
      ),
    );
  }
}

With this scaffold in place, you can explore each CRUD module (user_screen.dart, product_screen.dart, etc.) to see form handling, list rendering and data operations. Use Kokoshop as a reference for common Flutter patterns in building full-featured mobile applications.

Getting Started

Follow these steps to clone the KOKOSHOP-Flutter repository, install dependencies, run the app on an emulator or device, and produce a release APK or IPA.

Prerequisites

  • Flutter SDK (v3.0.0 or later)
  • Dart SDK (bundled with Flutter)
  • Android Studio / Xcode with emulator or device setup
  • Git

1. Clone the Repository

# Replace <path> if you want a custom location
git clone https://github.com/kevindluna/KOKOSHOP-Flutter.git
cd KOKOSHOP-Flutter

2. Install Dependencies

Retrieve Flutter and Dart packages:

flutter pub get

3. Run on Emulator or Device

Android

  1. Start an Android emulator via Android Studio or:
    flutter emulators --launch pixel_5_api_30
    
  2. Run the app:
    flutter run
    

iOS

  1. Open Xcode simulators:
    open -a Simulator
    
  2. Run the app:
    flutter run
    

Attach Physical Device

Connect your device via USB and ensure USB debugging (Android) or provisioning (iOS) is enabled. Then:

flutter run

4. Build Release APK

Generate an optimized Android package:

# Optional: set target platform
flutter build apk --release

Output file: build/app/outputs/flutter-apk/app-release.apk

5. Build Release IPA

  1. Configure iOS signing in ios/Runner.xcodeproj.
  2. From project root:
    flutter build ipa --export-options-plist=ios/ExportOptions.plist
    

Output file: build/ios/ipa/Runner.ipa

6. Main Entry Point

The app initializes in lib/main.dart. HomeScreen is the starting page. You can customize theme, routes, or initial arguments here:

// lib/main.dart
void main() {
  runApp(const KokoshopApp());
}

class KokoshopApp extends StatelessWidget {
  const KokoshopApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'KOKOSHOP',
      theme: ThemeData(
        primarySwatch: Colors.teal,
      ),
      home: const HomeScreen(), // Entry point for shopping management
    );
  }
}
## Data Layer & Models

This section explains how the app structures, stores, and accesses data via a local SQLite database and Dart model classes. It covers database initialization, CRUD utilities, and object ↔ Map serialization.

### DatabaseHelper & SQLite Schema

#### Initializing the Database  
DatabaseHelper manages a single SQLite instance, creates tables on first open, and exposes CRUD methods.

```dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final _dbName = 'kokoshop.db';
  static final _dbVersion = 1;
  static Database? _instance;

  Future<Database> get database async {
    if (_instance != null) return _instance!;
    final path = join(await getDatabasesPath(), _dbName);
    _instance = await openDatabase(
      path,
      version: _dbVersion,
      onCreate: _onCreate,
    );
    return _instance!;
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE usuario(
        id_cli   INTEGER PRIMARY KEY AUTOINCREMENT,
        nom_cli  TEXT NOT NULL,
        ap_cli   TEXT NOT NULL,
        tel_cli  TEXT NOT NULL,
        correo_cli TEXT NOT NULL,
        pass_cli TEXT NOT NULL
      );
    ''');
    await db.execute('''
      CREATE TABLE producto(
        id_producto   INTEGER PRIMARY KEY AUTOINCREMENT,
        nom_producto  TEXT NOT NULL,
        precio_producto REAL NOT NULL,
        stock_producto INTEGER NOT NULL,
        imagen_producto TEXT
      );
    ''');
    await db.execute('''
      CREATE TABLE carrito(
        id_carrito INTEGER PRIMARY KEY AUTOINCREMENT,
        id_cli     INTEGER NOT NULL,
        FOREIGN KEY(id_cli) REFERENCES usuario(id_cli)
      );
    ''');
    await db.execute('''
      CREATE TABLE productos_carrito(
        id_carrito       INTEGER NOT NULL,
        id_producto      INTEGER NOT NULL,
        cantidad_product INTEGER NOT NULL,
        PRIMARY KEY(id_carrito, id_producto),
        FOREIGN KEY(id_carrito)  REFERENCES carrito(id_carrito),
        FOREIGN KEY(id_producto) REFERENCES producto(id_producto)
      );
    ''');
    await db.execute('''
      CREATE TABLE venta(
        id_venta    INTEGER PRIMARY KEY AUTOINCREMENT,
        id_cli      INTEGER NOT NULL,
        fecha_venta TEXT NOT NULL,
        tipo_pago   TEXT NOT NULL,
        estado      TEXT NOT NULL,
        FOREIGN KEY(id_cli) REFERENCES usuario(id_cli)
      );
    ''');
    await db.execute('''
      CREATE TABLE productos_venta(
        id_venta     INTEGER NOT NULL,
        id_producto  INTEGER NOT NULL,
        cantidad_venta INTEGER NOT NULL,
        PRIMARY KEY(id_venta, id_producto),
        FOREIGN KEY(id_venta)    REFERENCES venta(id_venta),
        FOREIGN KEY(id_producto) REFERENCES producto(id_producto)
      );
    ''');
  }
}

Common CRUD Patterns

Each entity has insert, query, update, and delete methods. They all follow this pattern:

// Insert an object into its table
Future<int> insert<T>(String table, Map<String, dynamic> data) async {
  final db = await database;
  return await db.insert(table, data);
}

// Query rows and map to model
Future<List<Map<String, dynamic>>> query(String table,
    {String? where, List<dynamic>? args}) async {
  final db = await database;
  return await db.query(table, where: where, whereArgs: args);
}

// Update existing row(s)
Future<int> update(String table, Map<String, dynamic> data,
    {required String where, required List<dynamic> args}) async {
  final db = await database;
  return await db.update(table, data, where: where, whereArgs: args);
}

// Delete row(s)
Future<int> delete(String table,
    {required String where, required List<dynamic> args}) async {
  final db = await database;
  return await db.delete(table, where: where, whereArgs: args);
}

You can wrap these in type-specific methods, e.g.:

Future<int> insertUsuario(Usuario u) =>
  insert('usuario', u.toMap());

Future<List<Usuario>> getAllUsuarios() async {
  final rows = await query('usuario');
  return rows.map((r) => Usuario.fromMap(r)).toList();
}

Data Models & Serialization

All models implement:

  • factory X.fromMap(Map<String, dynamic> m)
  • Map<String, dynamic> toMap()

Carrito

class Carrito {
  int? id_carrito;
  int  id_cli;

  Carrito({this.id_carrito, required this.id_cli});

  factory Carrito.fromMap(Map<String, dynamic> m) => Carrito(
    id_carrito: m['id_carrito'] as int?,
    id_cli:      m['id_cli']      as int,
  );

  Map<String, dynamic> toMap() => {
    'id_carrito': id_carrito,
    'id_cli':     id_cli,
  };
}

Producto

class Producto {
  int?    id_producto;
  String  nom_producto;
  double  precio_producto;
  int     stock_producto;
  String? imagen_producto;

  Producto({
    this.id_producto,
    required this.nom_producto,
    required this.precio_producto,
    required this.stock_producto,
    this.imagen_producto,
  });

  factory Producto.fromMap(Map<String, dynamic> m) => Producto(
    id_producto:     m['id_producto']   as int?,
    nom_producto:    m['nom_producto']  as String,
    precio_producto: m['precio_producto'] as double,
    stock_producto:  m['stock_producto']  as int,
    imagen_producto: m['imagen_producto'] as String?,
  );

  Map<String, dynamic> toMap() => {
    'id_producto':    id_producto,
    'nom_producto':   nom_producto,
    'precio_producto':precio_producto,
    'stock_producto': stock_producto,
    'imagen_producto':imagen_producto,
  };
}

Usuario

class Usuario {
  int?    id_cli;
  String  nom_cli;
  String  ap_cli;
  String  tel_cli;
  String  correo_cli;
  String  pass_cli;

  Usuario({
    this.id_cli,
    required this.nom_cli,
    required this.ap_cli,
    required this.tel_cli,
    required this.correo_cli,
    required this.pass_cli,
  });

  factory Usuario.fromMap(Map<String, dynamic> m) => Usuario(
    id_cli:     m['id_cli']     as int?,
    nom_cli:    m['nom_cli']    as String,
    ap_cli:     m['ap_cli']     as String,
    tel_cli:    m['tel_cli']    as String,
    correo_cli: m['correo_cli'] as String,
    pass_cli:   m['pass_cli']   as String,
  );

  Map<String, dynamic> toMap() => {
    'id_cli':     id_cli,
    'nom_cli':    nom_cli,
    'ap_cli':     ap_cli,
    'tel_cli':    tel_cli,
    'correo_cli': correo_cli,
    'pass_cli':   pass_cli,
  };
}

Venta

class Venta {
  int?    id_venta;
  int     id_cli;
  String  fecha_venta;
  String  tipo_pago;
  String  estado;

  Venta({
    this.id_venta,
    required this.id_cli,
    required this.fecha_venta,
    required this.tipo_pago,
    required this.estado,
  });

  factory Venta.fromMap(Map<String, dynamic> m) => Venta(
    id_venta:    m['id_venta']    as int?,
    id_cli:      m['id_cli']      as int,
    fecha_venta: m['fecha_venta'] as String,
    tipo_pago:   m['tipo_pago']   as String,
    estado:      m['estado']      as String,
  );

  Map<String, dynamic> toMap() => {
    'id_venta':    id_venta,
    'id_cli':      id_cli,
    'fecha_venta': fecha_venta,
    'tipo_pago':   tipo_pago,
    'estado':      estado,
  };
}

ProductoCarrito

class ProductoCarrito {
  final int id_carrito;
  final int id_producto;
  int cantidad_product;

  ProductoCarrito({
    required this.id_carrito,
    required this.id_producto,
    required this.cantidad_product,
  });

  factory ProductoCarrito.fromMap(Map<String, dynamic> m) =>
    ProductoCarrito(
      id_carrito:       m['id_carrito']       as int,
      id_producto:      m['id_producto']      as int,
      cantidad_product: m['cantidad_product'] as int,
    );

  Map<String, dynamic> toMap() => {
    'id_carrito':       id_carrito,
    'id_producto':      id_producto,
    'cantidad_product': cantidad_product,
  };
}

ProductoVenta

class ProductoVenta {
  final int id_venta;
  final int id_producto;
  int cantidad_venta;

  ProductoVenta({
    required this.id_venta,
    required this.id_producto,
    required this.cantidad_venta,
  });

  factory ProductoVenta.fromMap(Map<String, dynamic> m) =>
    ProductoVenta(
      id_venta:      m['id_venta']      as int,
      id_producto:   m['id_producto']   as int,
      cantidad_venta:m['cantidad_venta']as int,
    );

  Map<String, dynamic> toMap() => {
    'id_venta':      id_venta,
    'id_producto':   id_producto,
    'cantidad_venta':cantidad_venta,
  };
}

Practical Usage

  1. Create a new cart for user-ID 3:
    final cart = Carrito(id_cli: 3);
    cart.id_carrito = await dbHelper.insert('carrito', cart.toMap());
    
  2. Add a product to cart:
    final item = ProductoCarrito(
      id_carrito: cart.id_carrito!,
      id_producto: 10,
      cantidad_product: 2,
    );
    await dbHelper.insert('productos_carrito', item.toMap());
    
  3. Fetch and display cart items:
    final rows = await dbHelper.query(
      'productos_carrito',
      where: 'id_carrito = ?', args: [cart.id_carrito],
    );
    final items = rows.map((r) => ProductoCarrito.fromMap(r)).toList();
    items.forEach((pc) => print('${pc.id_producto}: ${pc.cantidad_product}'));
    
  4. Finalize a sale:
    final venta = Venta(
      id_cli: cart.id_cli,
      fecha_venta: DateTime.now().toIso8601String(),
      tipo_pago: 'Tarjeta',
      estado: 'Pendiente',
    );
    venta.id_venta = await dbHelper.insert('venta', venta.toMap());
    items.forEach((pc) {
      final pv = ProductoVenta(
        id_venta: venta.id_venta!,
        id_producto: pc.id_producto,
        cantidad_venta: pc.cantidad_product,
      );
      dbHelper.insert('productos_venta', pv.toMap());
    });
    

By following these patterns, you keep business logic clean, ensure consistent serialization, and leverage SQLite for reliable offline data management.

UI Screens & Workflows

This section describes each major screen in KOKOSHOP-Flutter, its navigation flow, and the CRUD interactions it drives. Use these patterns to modify UI components or extend features.


HomeScreen – App Navigation Hub

Provides entry points to all CRUD modules via Navigator.push.

Structure

  • Scaffold with AppBar(title: 'CRUD Kokoshop')
  • Centered Column of ElevatedButton widgets
  • Each button pushes a target screen

Example

import 'package:flutter/material.dart';
import 'usuario_screen.dart';
import 'producto_screen.dart';
import 'ventas_screen.dart';
import 'carrito_screen.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CRUD Kokoshop')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _NavButton(label: 'Usuarios', destination: UsuarioScreen(), context: context),
            _NavButton(label: 'Productos', destination: ProductoScreen(), context: context),
            _NavButton(label: 'Ventas', destination: VentasScreen(), context: context),
            _NavButton(label: 'Carritos', destination: CarritoScreen(), context: context),
          ],
        ),
      ),
    );
  }
}

class _NavButton extends StatelessWidget {
  final String label;
  final Widget destination;
  final BuildContext context;
  _NavButton({required this.label, required this.destination, required this.context});
  @override
  Widget build(BuildContext _) => Padding(
    padding: EdgeInsets.symmetric(vertical: 8),
    child: ElevatedButton(
      onPressed: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => destination),
      ),
      child: Text(label),
    ),
  );
}

Guidance

  • Add new modules by copying _NavButton.
  • Switch to named routes: define in MaterialApp(routes:{}) and call Navigator.pushNamed.
  • Test navigation with a custom NavigatorObserver.

UsuarioScreen – User Management

Manages Usuario records: list, create, edit, delete.

Workflow

  1. Load users via FutureBuilder calling _dbHelper.getUsuarios().
  2. Display each user in a ListTile with edit/delete icons.
  3. Show modal bottom sheet with _showForm(int? id) for create/edit.
  4. After insert/update/delete, call _refreshUsuarios() to rebuild.

Key UI & Code

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Usuarios')),
    body: FutureBuilder<List<Usuario>>(
      future: _dbHelper.getUsuarios(),
      builder: (_, snapshot) {
        if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
        final users = snapshot.data!;
        return ListView.builder(
          itemCount: users.length,
          itemBuilder: (_, i) => ListTile(
            title: Text('${users[i].nombre} ${users[i].apellido}'),
            subtitle: Text(users[i].correo),
            trailing: Row(mainAxisSize: MainAxisSize.min, children: [
              IconButton(
                icon: Icon(Icons.edit),
                onPressed: () => _showForm(users[i].ID),
              ),
              IconButton(
                icon: Icon(Icons.delete),
                onPressed: () async {
                  await _dbHelper.deleteUsuario(users[i].ID!);
                  _refreshUsuarios();
                },
              ),
            ]),
          ),
        );
      },
    ),
    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () => _showForm(null),
    ),
  );
}

ProductoScreen – Product Management

Handles Producto records using a modal bottom sheet for create/update.

Workflow

  1. Fetch products via getProductos() in FutureBuilder.
  2. Tap FAB to call _showForm(null); tap edit icon to call _showForm(id).
  3. _showForm(int? id) preloads data when id != null, displays TextFormFields, validates, then calls insertProducto or updateProducto.
  4. Refresh list via setState() after DB operations.

Form Snippet

void _showForm(int? id) async {
  if (id != null) {
    final p = (await _dbHelper.getProductos()).firstWhere((p) => p.id_producto == id);
    _nombre = p.nombre;
    _cantidad = p.cantidad;
    _precio = p.precio;
    _tipo = p.tipo_producto;
  }
  showModalBottomSheet(
    context: context,
    builder: (_) => Padding(
      padding: EdgeInsets.all(16),
      child: Form(
        key: _formKey,
        child: Column(mainAxisSize: MainAxisSize.min, children: [
          TextFormField(
            initialValue: _nombre,
            decoration: InputDecoration(labelText: 'Nombre'),
            validator: (v) => v!.isEmpty ? 'Ingrese nombre' : null,
            onSaved: (v) => _nombre = v!,
          ),
          // cantidad, precio, tipo fields...
          SizedBox(height: 20),
          ElevatedButton(
            child: Text(id == null ? 'Agregar' : 'Actualizar'),
            onPressed: () async {
              if (!_formKey.currentState!.validate()) return;
              _formKey.currentState!.save();
              if (id == null) {
                await _dbHelper.insertProducto(Producto(
                  nombre: _nombre,
                  cantidad: _cantidad,
                  precio: _precio,
                  tipo_producto: _tipo,
                ));
              } else {
                await _dbHelper.updateProducto(Producto(
                  id_producto: id,
                  nombre: _nombre,
                  cantidad: _cantidad,
                  precio: _precio,
                  tipo_producto: _tipo,
                ));
              }
              setState(() {});
              Navigator.pop(context);
            },
          ),
        ]),
      ),
    ),
  );
}

VentasScreen – Sales Management

Manages Venta records and navigates to sale’s product screen.

Workflow

  1. Load sales via getVentas() in FutureBuilder.
  2. Edit/create via _showForm(int? id), similar to Usuario and Producto.
  3. Navigate to ProductosVentaScreen(venta) on tap of a sale entry.

List & Navigation

ListView.builder(
  itemCount: ventas.length,
  itemBuilder: (_, i) {
    final v = ventas[i];
    return ListTile(
      title: Text('Venta #${v.Id_venta} - Cliente ${v.Id_usuario}'),
      subtitle: Text(v.fechaVenta),
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => ProductosVentaScreen(venta: v)),
      ),
      trailing: IconButton(
        icon: Icon(Icons.delete),
        onPressed: () async {
          await _dbHelper.deleteVentas(v.Id_venta!);
          _refreshVentas();
        },
      ),
    );
  },
);

CarritoScreen – Shopping Cart Management

CRUD for Carrito and navigation to cart items.

Workflow

  1. List carts from getCarrito().
  2. Create/edit via _showForm(int? idCarrito).
  3. Tap a cart entry to navigate to ProductosCarritoScreen(carrito: c).

List & Form Invocation

ListView.builder(
  itemCount: carritos.length,
  itemBuilder: (_, i) {
    final c = carritos[i];
    return ListTile(
      title: Text('Carrito #${c.id_carrito}'),
      subtitle: Text('Cliente: ${c.id_cli}'),
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => ProductosCarritoScreen(carrito: c)),
      ),
      trailing: IconButton(
        icon: Icon(Icons.edit),
        onPressed: () => _showForm(c.id_carrito),
      ),
    );
  },
);

ProductosCarritoScreen – Cart Items Workflow

Manages items inside a specific cart: listing, adding products.

Workflow

  1. Load existing cart items and available products in initState().
  2. Display current items in a ListView.
  3. Use two DropdownButtons to select Producto and quantity.
  4. On “Agregar” tap, call agregarProductoAlCarrito():
    • Validate selection
    • insertProductoCarrito
    • Reload items and show SnackBar

Add Logic

Future<void> agregarProductoAlCarrito() async {
  if (productoSeleccionado == null) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Selecciona un producto')),
    );
    return;
  }
  final linea = ProductoCarrito(
    id_carrito: widget.carrito.id_carrito!,
    id_producto: productoSeleccionado!.id_producto!,
    cantidad_product: cantidadSeleccionada,
  );
  await _dbHelper.insertProductoCarrito(linea);
  await cargarProductosCarrito();
  ScaffoldMessenger.of(context)
      .showSnackBar(SnackBar(content: Text('Producto agregado')));
}

ProductosVentaScreen – Sale Items Workflow

Similar to cart items but tied to a sale.

Workflow

  1. Load sale products and available products in initState().
  2. Build UI with existing items list.
  3. Select product and quantity; call agregarProductoALaVenta() to insert and reload.
  4. Use SnackBar for feedback.

Add Logic

Future<void> agregarProductoALaVenta() async {
  if (productoSeleccionado == null) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Selecciona un producto')),
    );
    return;
  }
  final linea = ProductoVenta(
    id_venta: widget.venta.Id_venta!,
    id_producto: productoSeleccionado!.id_producto!,
    cantidad: cantidadSeleccionada,
  );
  await _dbHelper.insertProductoVenta(linea);
  await cargarProductosVenta();
  ScaffoldMessenger.of(context)
      .showSnackBar(SnackBar(content: Text('Producto agregado a la venta')));
}

Practical Tips

  • Always call your reload methods (_refreshUsuarios(), setState(), cargar…()) after DB changes to keep UI in sync.
  • Keep form-state variables at the State level to persist across rebuilds.
  • Wrap your modal forms in SingleChildScrollView and Padding for keyboard compatibility.
  • Use consistent validation patterns for numeric vs. text inputs.
  • Extract common widgets (e.g., dropdown+label) to reduce duplication across screens.