diff --git a/README.md b/README.md index 6426549..dd32e89 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,38 @@ CoffeeShop app preview: ![Four screenshots for different screens in the app: product catalog, cart, user profile and order history](./imgs/workshop_preview.png) -## Requirements: - -### Required: +## What you'll learn + +By completing this workshop you will be able to: + +- Create a multi-screen Flutter app using `BottomNavigationBar` +- Build common UI layouts with widgets such as `Column`, `ListView`, `ListTile`, `Card`, and `CircleAvatar` +- Navigate between pages (both tab-based and stack-based with `Navigator.push`) +- Define simple Dart data models (classes with named constructors) +- Manage shared app state without an external library +- Use hot-reload to see live changes during development + +## Project structure + +``` +app/ +└── lib/ + ├── main.dart # App entry point and bottom-navigation host + ├── coffee_manager.dart # Lightweight in-memory state container + ├── models/ + │ ├── product.dart # Product data model + │ ├── user.dart # User data model + │ └── order.dart # Order data model + └── pages/ + ├── catalog.dart # Product listing screen + ├── cart.dart # Shopping cart screen + ├── profile.dart # User profile screen + └── orders.dart # Order history screen +``` + +## Requirements + +### Required - Bring Your Own Device (BYOD) - Basic/medium programming knowledge @@ -30,7 +59,30 @@ To choose: - Android emulator device - Physical device (developer mode and USB debugging enabled) -## About code: +## Running the app + +```bash +# Navigate to the app directory +cd app + +# Fetch dependencies +flutter pub get + +# Run on a connected device or emulator +flutter run +``` + +To add support for additional platforms: + +```bash +# Web +flutter create --platforms=web . + +# Linux +flutter create --platforms=linux . +``` + +## About the code No state management libraries/tools/engines are used. @@ -46,6 +98,11 @@ This workshop focuses on the development of UI, to learn the basics of Flutter, Clone this repo and checkout to [`workshop`](https://github.com/Beelzenef/workshop_flutter/tree/workshop) branch. +> 💡 Don't want to use Git? Download the starter project directly from +> [download-directory.github.io](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fzelonware%2Fworkshop_flutter%2Ftree%2Fworkshop%2Fapp). + +For a detailed step-by-step guide see [`docs/guide.md`](./docs/guide.md). + ## More info General: diff --git a/app/README.md b/app/README.md index f8d58e0..d9036ea 100644 --- a/app/README.md +++ b/app/README.md @@ -1,3 +1,14 @@ # coffee_shop -A new Flutter app! \ No newline at end of file +# CoffeeShop – Flutter Workshop App + +A beginner-friendly Flutter application built during a hands-on workshop. It demonstrates core Flutter concepts including multi-screen navigation, stateful widgets, and simple in-memory state management. + +## Getting started + +```bash +flutter pub get +flutter run +``` + +See the [root README](../README.md) and [workshop guide](../docs/guide.md) for full setup instructions and the step-by-step workshop walkthrough. diff --git a/app/lib/coffee_manager.dart b/app/lib/coffee_manager.dart index e4503c8..bf1d4ec 100644 --- a/app/lib/coffee_manager.dart +++ b/app/lib/coffee_manager.dart @@ -2,20 +2,39 @@ import 'package:coffee_shop/models/order.dart'; import 'package:coffee_shop/models/product.dart'; import 'package:coffee_shop/models/user.dart'; +/// Lightweight in-memory state container for the CoffeeShop app. +/// +/// [CoffeeManager] holds all mutable application data – the current user, +/// the shopping cart, and the order history – and exposes simple methods to +/// manipulate that data. A single instance is created in [CoffeeShopApp] and +/// shared across all pages through the static [CoffeeShopApp.manager] accessor. +/// +/// > **Note:** This class does **not** use any state management library and +/// > does **not** persist data between app restarts. It is intentionally simple +/// > for workshop / learning purposes. class CoffeeManager { + /// The currently logged-in user. Pre-populated with placeholder data. final User user = User(name: 'Awesome', mail: 'you@awesome.com', phone: '666-666-66'); + /// Products that have been added to the cart but not yet paid for. final List productsInCart = []; + + /// Completed orders placed during the current session. final List orders = []; + /// Running total of all items currently in [productsInCart], in whole + /// currency units. Updated automatically whenever the cart changes. int total = 0; + /// Adds [product] to the cart and recalculates [total]. void addToCart(Product product) { productsInCart.add(product); _calculateTotal(); } + /// Recalculates [total] by summing the prices of all items in [productsInCart]. + /// Resets [total] to 0 when the cart is empty. void _calculateTotal() { if (productsInCart.isNotEmpty) { var prices = productsInCart.map((e) => e.price).toList(); @@ -25,6 +44,8 @@ class CoffeeManager { } } + /// Converts the current cart into a new [Order], appends it to [orders], + /// and clears the cart. Does nothing if the cart is empty. void makeOrder() { if (productsInCart.isNotEmpty) { int howManyProducts = productsInCart.length; @@ -37,6 +58,7 @@ class CoffeeManager { } } + /// Removes all products from the cart and resets [total] to 0. void clearCart() { productsInCart.clear(); _calculateTotal(); diff --git a/app/lib/main.dart b/app/lib/main.dart index 8c737df..ee1af02 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -9,9 +9,15 @@ void main() { runApp(const CoffeeShopApp()); } +/// Root widget of the CoffeeShop application. +/// +/// Configures [MaterialApp] with the app theme and sets [HomePage] as the +/// initial route. A single shared [CoffeeManager] instance is stored here and +/// accessed by child widgets via `CoffeeShopApp.manager`. class CoffeeShopApp extends StatelessWidget { const CoffeeShopApp({super.key}); + /// Shared state manager accessible from any widget in the tree. static final manager = CoffeeManager(); @override @@ -28,9 +34,12 @@ class CoffeeShopApp extends StatelessWidget { } } +/// The main scaffold of the app, hosting the [BottomNavigationBar] and +/// switching between the three top-level pages. class HomePage extends StatefulWidget { const HomePage({super.key, required this.title}); + /// Title displayed in the [AppBar]. final String title; @override @@ -38,8 +47,14 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + /// Index of the currently selected bottom-navigation tab. int index = 0; + /// Returns the page widget that corresponds to the selected [index]. + /// + /// - 0 → [CatalogPage] + /// - 1 → [CartPage] + /// - 2 → [ProfilePage] Widget navigateToPage() { switch (index) { case 1: diff --git a/app/lib/models/order.dart b/app/lib/models/order.dart index a126ecb..524bc24 100644 --- a/app/lib/models/order.dart +++ b/app/lib/models/order.dart @@ -1,7 +1,15 @@ +/// Represents a completed purchase order. class Order { + /// Creates an [Order] with the number of [products] purchased, + /// the [total] price paid, and the [orderedAt] timestamp. Order({required this.products, required this.total, required this.orderedAt}); + /// Number of products included in this order. final int products; + + /// Total cost of the order in whole currency units. final int total; + + /// Date and time at which the order was placed. final DateTime orderedAt; } diff --git a/app/lib/models/product.dart b/app/lib/models/product.dart index 627a2a0..cb60840 100644 --- a/app/lib/models/product.dart +++ b/app/lib/models/product.dart @@ -1,7 +1,15 @@ +/// Represents a coffee product available in the catalog. class Product { + /// Creates a [Product] with a [name], [price] in whole currency units, + /// and a [pic] URL (may be empty in the workshop starter). Product({required this.name, required this.price, required this.pic}); + /// Display name of the product (e.g. "Espresso"). final String name; + + /// Price of the product in whole currency units (e.g. 2 for €2). final int price; + + /// URL or asset path for the product image. May be an empty string. final String pic; } diff --git a/app/lib/models/user.dart b/app/lib/models/user.dart index 7c5fb66..bbbbc65 100644 --- a/app/lib/models/user.dart +++ b/app/lib/models/user.dart @@ -1,7 +1,14 @@ +/// Represents the logged-in user of the coffee shop app. class User { + /// Creates a [User] with a display [name], [mail] address and [phone] number. User({required this.name, required this.mail, required this.phone}); + /// Display name shown in the profile and catalog greeting. final String name; + + /// Email address of the user. final String mail; + + /// Phone number of the user. final String phone; } diff --git a/app/lib/pages/cart.dart b/app/lib/pages/cart.dart index f8b8941..55fd20b 100644 --- a/app/lib/pages/cart.dart +++ b/app/lib/pages/cart.dart @@ -2,6 +2,13 @@ import 'package:coffee_shop/main.dart'; import 'package:flutter/material.dart'; +/// Displays the products currently in the shopping cart together with the +/// running total. Provides **PAY** and **REMOVE ORDERS** actions. +/// +/// - **PAY** calls [CoffeeManager.makeOrder], which converts the cart into a +/// completed [Order] and clears it. +/// - **REMOVE ORDERS** calls [CoffeeManager.clearCart], discarding all items +/// without creating an order. class CartPage extends StatefulWidget { const CartPage({super.key}); diff --git a/app/lib/pages/catalog.dart b/app/lib/pages/catalog.dart index 791f02f..d135db5 100644 --- a/app/lib/pages/catalog.dart +++ b/app/lib/pages/catalog.dart @@ -3,6 +3,8 @@ import 'package:coffee_shop/models/product.dart'; import 'package:flutter/material.dart'; +/// Displays the list of available coffee products and allows the user to add +/// items to the cart by tapping them. class CatalogPage extends StatefulWidget { const CatalogPage({super.key}); @@ -43,6 +45,10 @@ class _CatalogPageState extends State { ); } + /// Builds a single [ListTile] for [p]. + /// + /// Tapping the tile calls [CoffeeManager.addToCart] and triggers a + /// `setState` so that any UI depending on the cart is rebuilt. ListTile buildCatalogItem(Product p) { String price = p.price.toString(); @@ -56,6 +62,7 @@ class _CatalogPageState extends State { ); } + /// Shows a [SnackBar] confirming that a product was added to the cart. void showToast(BuildContext context) { final scaffold = ScaffoldMessenger.of(context); scaffold.showSnackBar( diff --git a/app/lib/pages/orders.dart b/app/lib/pages/orders.dart index 1839373..fad3c17 100644 --- a/app/lib/pages/orders.dart +++ b/app/lib/pages/orders.dart @@ -3,8 +3,16 @@ import 'package:coffee_shop/models/order.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +/// Shows the full list of completed orders for the current session. +/// +/// Receives the [orders] list from [ProfilePage] and renders each entry as a +/// coloured card showing the number of products, total cost, and the formatted +/// date/time of the purchase. class OrdersPage extends StatelessWidget { + /// The list of orders to display. final List orders; + + /// Formatter used to display [Order.orderedAt] as `dd-MM-yyyy (HH:mm)`. final DateFormat formatter = DateFormat('dd-MM-yyyy (HH:mm)'); OrdersPage({super.key, required this.orders}); diff --git a/app/lib/pages/profile.dart b/app/lib/pages/profile.dart index 83a56b0..ee38d97 100644 --- a/app/lib/pages/profile.dart +++ b/app/lib/pages/profile.dart @@ -3,6 +3,11 @@ import 'package:coffee_shop/main.dart'; import 'package:flutter/material.dart'; +/// Displays the current user's profile information. +/// +/// Shows a placeholder avatar, the user's name, email address, and phone +/// number read from [CoffeeManager.user]. When at least one order has been +/// placed, an **All orders** button appears that navigates to [OrdersPage]. class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); diff --git a/docs/guide.md b/docs/guide.md index 99e2912..e8c9ba6 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,4 +1,114 @@ -# Guía +# Workshop Guide + +> 🇪🇸 A Spanish version of this guide is available below / Una versión en español de esta guía está disponible a continuación. + +--- + +## Premises + +- App for a coffee shop +- Unlimited resources (💵💶💷💴) – no payment processing needed +- Infinite products can be added to the cart +- No user profile customisation +- Everything runs locally – no data is persisted between sessions + +## Getting the starter project + +**With Git:** + +```bash +git clone https://github.com/Beelzenef/workshop_flutter.git +cd workshop_flutter +git checkout workshop +``` + +**Without Git:** + +Download the starter project directly from [download-directory.github.io](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fzelonware%2Fworkshop_flutter%2Ftree%2Fworkshop%2Fapp). + +## Running the project + +Open the `app/` folder with **Visual Studio Code** or **Android Studio**, then run: + +```bash +flutter pub get +flutter run +``` + +## Enable additional platforms + +Add web support: + +```bash +flutter create --platforms=web . +``` + +Add Linux support: + +```bash +flutter create --platforms=linux . +``` + +## Step-by-step guide + +### 1. Explore `coffee_manager.dart` + +Open `lib/coffee_manager.dart`. This file contains the `CoffeeManager` class which acts as a simple in-memory data store for the app. It holds: + +- The logged-in `User` +- The list of `Product` items currently in the cart (`productsInCart`) +- The list of completed `Order` objects (`orders`) +- A running cart `total` + +Feel free to customise the default user data. + +### 2. Remove the debug banner + +In `lib/main.dart`, notice the `MaterialApp` widget. Set `debugShowCheckedModeBanner: false` to hide the red *DEBUG* ribbon. Use **hot reload** (press `r` in the terminal, or save the file in your editor) to see the change instantly. + +### 3. Add the page files + +Create the following files inside `lib/pages/`: + +| File | Purpose | +|------|---------| +| `catalog.dart` | Lists available coffee products | +| `cart.dart` | Shows items currently in the cart | +| `profile.dart` | Displays user information | + +Each file should contain a `StatefulWidget` subclass with the matching name (e.g. `CatalogPage`). + +### 4. Implement `BottomNavigationBar` + +In `lib/main.dart`, update `HomePage` to include a `BottomNavigationBar` with three tabs: + +- **Catalog** – navigates to `CatalogPage` +- **Cart** – navigates to `CartPage` +- **Profile** – navigates to `ProfilePage` + +Use a `switch` statement (or `if/else`) to return the correct page widget based on the currently selected tab index. + +### 5. Add the data models + +Create the following files inside `lib/models/`: + +| File | Class | Fields | +|------|-------|--------| +| `product.dart` | `Product` | `name` (String), `price` (int), `pic` (String) | +| `user.dart` | `User` | `name` (String), `mail` (String), `phone` (String) | +| `order.dart` | `Order` | `products` (int), `total` (int), `orderedAt` (DateTime) | + +### 6. Wire everything together + +- In `CatalogPage`, read the product list and call `CoffeeShopApp.manager.addToCart(product)` when a tile is tapped. +- In `CartPage`, display `CoffeeShopApp.manager.productsInCart` and provide **PAY** and **REMOVE** buttons that call `manager.makeOrder()` and `manager.clearCart()`. +- In `ProfilePage`, show the user's name, email, and phone. If there are completed orders, show a button that navigates to an `OrdersPage`. + +--- + +--- + +# Guía (Español) ## Premisas