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
- Start an Android emulator via Android Studio or:
flutter emulators --launch pixel_5_api_30
- Run the app:
flutter run
iOS
- Open Xcode simulators:
open -a Simulator
- 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
- Configure iOS signing in
ios/Runner.xcodeproj
. - 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
- Create a new cart for user-ID
3
:final cart = Carrito(id_cli: 3); cart.id_carrito = await dbHelper.insert('carrito', cart.toMap());
- 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());
- 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}'));
- 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
withAppBar(title: 'CRUD Kokoshop')
- Centered
Column
ofElevatedButton
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 callNavigator.pushNamed
. - Test navigation with a custom
NavigatorObserver
.
UsuarioScreen – User Management
Manages Usuario
records: list, create, edit, delete.
Workflow
- Load users via
FutureBuilder
calling_dbHelper.getUsuarios()
. - Display each user in a
ListTile
with edit/delete icons. - Show modal bottom sheet with
_showForm(int? id)
for create/edit. - 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
- Fetch products via
getProductos()
inFutureBuilder
. - Tap FAB to call
_showForm(null)
; tap edit icon to call_showForm(id)
. _showForm(int? id)
preloads data whenid != null
, displaysTextFormField
s, validates, then callsinsertProducto
orupdateProducto
.- 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
- Load sales via
getVentas()
inFutureBuilder
. - Edit/create via
_showForm(int? id)
, similar to Usuario and Producto. - 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
- List carts from
getCarrito()
. - Create/edit via
_showForm(int? idCarrito)
. - 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
- Load existing cart items and available products in
initState()
. - Display current items in a
ListView
. - Use two
DropdownButton
s to selectProducto
and quantity. - 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
- Load sale products and available products in
initState()
. - Build UI with existing items list.
- Select product and quantity; call
agregarProductoALaVenta()
to insert and reload. - 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
andPadding
for keyboard compatibility. - Use consistent validation patterns for numeric vs. text inputs.
- Extract common widgets (e.g., dropdown+label) to reduce duplication across screens.