Screens / commerce
Product page
Presents a product with its photo gallery, price, rating, color, size and quantity, a collapsible description and add to cart always in reach, with loading, sold out, error and offline states.
Import
Product page is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:
fliin login
fliin add productAlso installs: accordion, alert, app-bar, button, close-button, core, empty-state, field, icons, page, page-indicator, result-card, section-header, segmented, skeleton, stepper, tag-group, text, text-field
Examples
Sale
A reduced price: compareAtPrice shows the old one struck through after it, formatted by your app like the price.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductSaleExample extends StatelessWidget {
const ProductSaleExample({super.key});
@override
Widget build(BuildContext context) {
const p = sampleProduct;
final product = Product(
id: p.id,
title: p.title,
brand: p.brand,
price: '39,00 €',
compareAtPrice: '49,00 €',
rating: p.rating,
reviews: p.reviews,
photos: p.photos,
description: p.description,
colors: p.colors,
maxQuantity: p.maxQuantity,
);
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: p.id,
initial: ProductReady(product, color: 'sand'),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Ready
The product: gallery, price, color and quantity, and the action that adds it to the cart. standalone holds its own controller over the service you pass.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductReadyExample extends StatelessWidget {
const ProductReadyExample({super.key});
@override
Widget build(BuildContext context) {
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: sampleProduct.id,
initial: const ProductReady(sampleProduct, color: 'sand'),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Loading
While the product loads, placeholders hold the place of the gallery and the details.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductLoadingExample extends StatelessWidget {
const ProductLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: sampleProduct.id,
initial: const ProductLoading(),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Unavailable
A sold-out product: the add action is disabled and says why.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductUnavailableExample extends StatelessWidget {
const ProductUnavailableExample({super.key});
@override
Widget build(BuildContext context) {
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: sampleProduct.id,
initial: const ProductReady(sampleSoldOutProduct),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Error
When loading fails, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductErrorExample extends StatelessWidget {
const ProductErrorExample({super.key});
@override
Widget build(BuildContext context) {
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: sampleProduct.id,
initial: const ProductFailed(),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Offline
Without a connection, the screen asks to reconnect rather than showing an error.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductOfflineExample extends StatelessWidget {
const ProductOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return ProductScreen.standalone(
service: const SampleProductService(latency: Duration(milliseconds: 600)),
productId: sampleProduct.id,
initial: const ProductOffline(),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: () {},
onOpenProduct: (_) {},
);
}
}Routing
fliin add product prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:
go_router
// lib/product_route.dart, next to product_page.dart.
import 'package:go_router/go_router.dart';
import 'product_page.dart';
/// Add productRoute to the routes of your GoRouter.
final productRoute = GoRoute(
path: '/product/:id',
builder: (context, state) => ProductPage(
id: state.pathParameters['id']!,
// Alone in the stack (deep link, go): your app's home.
onBack: () => context.canPop() ? context.pop() : context.go('/'),
// The cart: Fliin's checkout screen, or yours.
onAdded: () => context.push('/checkout'),
onOpenProduct: (product) => context.push('/product/${Uri.encodeComponent(product.id)}'),
),
);navigator
// lib/product_route.dart, next to product_page.dart.
import 'package:flutter/material.dart';
import 'product_page.dart';
/// Opens “Product page”: `Navigator.of(context).push(productRoute(id, cartPage: …))`.
/// [cartPage] builds the cart: the page of Fliin's checkout screen with its callbacks, or yours (checkoutRoute cannot come from here, checkout leads to order).
Route<void> productRoute(String id, {required WidgetBuilder cartPage}) => MaterialPageRoute<void>(
builder: (context) => ProductPage(
id: id,
onBack: () => Navigator.of(context).maybePop(),
onAdded: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: cartPage)),
onOpenProduct: (product) => Navigator.of(context).push(productRoute(product.id, cartPage: cartPage)),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add product prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:
none
// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
class ProductPage extends StatelessWidget {
const ProductPage({
super.key,
required this.id,
required this.onBack,
required this.onAdded,
required this.onOpenProduct,
});
final String id;
final VoidCallback onBack;
final VoidCallback onAdded;
final ValueChanged<ProductSummary> onOpenProduct;
@override
Widget build(BuildContext context) => ProductScreen.standalone(
key: ValueKey(id),
service: const SampleProductService(),
productId: id,
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: onBack,
onAdded: onAdded,
onOpenProduct: onOpenProduct,
);
}provider
// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class ProductPage extends StatelessWidget {
const ProductPage({
super.key,
required this.id,
required this.onBack,
required this.onAdded,
required this.onOpenProduct,
});
final String id;
final VoidCallback onBack;
final VoidCallback onAdded;
final ValueChanged<ProductSummary> onOpenProduct;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => ProductController(service: const SampleProductService(), productId: id)..load(),
child: Builder(
builder: (context) => ProductScreen(
controller: context.read<ProductController>(),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: onBack,
onAdded: onAdded,
onOpenProduct: onOpenProduct,
),
),
);
}riverpod
// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final productControllerProvider = Provider.autoDispose.family<ProductController, String>((ref, id) {
final controller = ProductController(service: const SampleProductService(), productId: id)..load();
ref.onDispose(controller.dispose);
return controller;
});
class ProductPage extends ConsumerWidget {
const ProductPage({
super.key,
required this.id,
required this.onBack,
required this.onAdded,
required this.onOpenProduct,
});
final String id;
final VoidCallback onBack;
final VoidCallback onAdded;
final ValueChanged<ProductSummary> onOpenProduct;
@override
Widget build(BuildContext context, WidgetRef ref) => ProductScreen(
controller: ref.watch(productControllerProvider(id)),
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: onBack,
onAdded: onAdded,
onOpenProduct: onOpenProduct,
);
}bloc
// lib/product_page.dart
// Your backend: implement ProductService (product_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/product/product_controller.dart';
import 'package:your_app/ui/fliin/screens/product/product_data.dart';
import 'package:your_app/ui/fliin/screens/product/product_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class ProductCubit extends Cubit<({ProductState state, int version})> {
ProductCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final ProductController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class ProductPage extends StatelessWidget {
const ProductPage({
super.key,
required this.id,
required this.onBack,
required this.onAdded,
required this.onOpenProduct,
});
final String id;
final VoidCallback onBack;
final VoidCallback onAdded;
final ValueChanged<ProductSummary> onOpenProduct;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => ProductCubit(ProductController(service: const SampleProductService(), productId: id)..load()),
child: Builder(
builder: (context) => ProductScreen(
controller: context.read<ProductCubit>().controller,
favoriteIcon: const SampleFavoriteIcon(),
favoriteSelectedIcon: const SampleFavoriteIcon(filled: true),
onBack: onBack,
onAdded: onAdded,
onOpenProduct: onOpenProduct,
),
),
);
}API
ProductScreen: class extends StatefulWidget
ProductScreen.new: const new({super.key, required ProductController controller, required Widget favoriteIcon, required Widget favoriteSelectedIcon, VoidCallback? onBack, VoidCallback? onAdded, ValueChanged<ProductSummary>? onOpenProduct})
ProductScreen.standalone: const new({super.key, required ProductService service, required String productId, required Widget favoriteIcon, required Widget favoriteSelectedIcon, ProductState? initial, VoidCallback? onBack, VoidCallback? onAdded, ValueChanged<ProductSummary>? onOpenProduct})
ProductScreen.controller: get ProductController?
ProductScreen.service: get ProductService?
ProductScreen.productId: get String?
ProductScreen.initial: get ProductState?
ProductScreen.favoriteIcon: get Widget
ProductScreen.favoriteSelectedIcon: get Widget
ProductScreen.onBack: get VoidCallback?
ProductScreen.onAdded: get VoidCallback?
ProductScreen.onOpenProduct: get ValueChanged<ProductSummary>?
ProductScreen.createState: State<ProductScreen>()
ProductState: sealed class permits {ProductFailed, ProductLoading, ProductOffline, ProductReady}
ProductState.new: const new()
ProductLoading: final class extends ProductState
ProductLoading.new: const new()
ProductFailed: final class extends ProductState
ProductFailed.new: const new()
ProductOffline: final class extends ProductState
ProductOffline.new: const new()
ProductNotice: enum {added, addFailed, addOffline, favoriteFailed, chooseSize}
ProductNotice.added: static final ProductNotice
ProductNotice.addFailed: static final ProductNotice
ProductNotice.addOffline: static final ProductNotice
ProductNotice.favoriteFailed: static final ProductNotice
ProductNotice.chooseSize: static final ProductNotice
ProductReady: final class extends ProductState
ProductReady.new: const new(Product product, {String? size, String? color, int quantity, bool adding, bool sizeMissing, ProductNotice? notice})
ProductReady.product: get Product
ProductReady.size: get String?
ProductReady.color: get String?
ProductReady.quantity: get int
ProductReady.adding: get bool
ProductReady.sizeMissing: get bool
ProductReady.notice: get ProductNotice?
ProductController: class extends ChangeNotifier
ProductController.new: new({required ProductService service, required String productId, ProductState initial})
ProductController.service: get ProductService
ProductController.productId: get String
ProductController.state: get ProductState
ProductController.load: Future<bool>()
ProductController.selectSize: bool(String id)
ProductController.selectColor: bool(String id)
ProductController.setQuantity: bool(int quantity)
ProductController.addToCart: Future<bool>()
ProductController.toggleFavorite: bool()
ProductController.dispose: void()
ProductPhoto: final class
ProductPhoto.new: const new(ImageProvider image, String description)
ProductPhoto.image: get ImageProvider
ProductPhoto.description: get String
ProductOption: final class
ProductOption.new: const new(String id, String label, {bool available})
ProductOption.id: get String
ProductOption.label: get String
ProductOption.available: get bool
ProductSummary: final class
ProductSummary.new: const new({required String id, required String title, required String price, ImageProvider? image})
ProductSummary.id: get String
ProductSummary.title: get String
ProductSummary.price: get String
ProductSummary.image: get ImageProvider?
Product: final class
Product.new: const new({required String id, required String title, required String price, required List<ProductPhoto> photos, required String description, String? brand, double? rating, int? reviews, String? details, String? delivery, List<ProductOption> sizes, List<ProductOption> colors, bool inStock, int maxQuantity, bool favorite, List<ProductSummary> similar, String? compareAtPrice})
Product.id: get String
Product.title: get String
Product.price: get String
Product.compareAtPrice: get String?
Product.photos: get List<ProductPhoto>
Product.description: get String
Product.brand: get String?
Product.rating: get double?
Product.reviews: get int?
Product.details: get String?
Product.delivery: get String?
Product.sizes: get List<ProductOption>
Product.colors: get List<ProductOption>
Product.inStock: get bool
Product.maxQuantity: get int
Product.favorite: get bool
Product.similar: get List<ProductSummary>
Product.withFavorite: Product(bool favorite)
CartLine: final class
CartLine.new: const new({required String productId, required int quantity, String? size, String? color})
CartLine.productId: get String
CartLine.quantity: get int
CartLine.size: get String?
CartLine.color: get String?
ProductNoNetwork: final class implements Exception
ProductNoNetwork.new: const new()
ProductService: abstract interface class
ProductService.new: new()
ProductService.load: abstract Future<Product>(String id)
ProductService.addToCart: abstract Future<void>(CartLine line)
ProductService.setFavorite: abstract Future<void>(String id, bool favorite)
sampleProduct: const dynamic
sampleSoldOutProduct: const dynamic
SampleProductService: final class implements ProductService
SampleProductService.new: const new({Product product, Duration latency})
SampleProductService.product: get Product
SampleProductService.latency: get Duration
SampleProductService.load: Future<Product>(String id)
SampleProductService.addToCart: Future<void>(CartLine line)
SampleProductService.setFavorite: Future<void>(String id, bool favorite)
SampleFavoriteIcon: class extends StatelessWidget
SampleFavoriteIcon.new: const new({super.key, bool filled})
SampleFavoriteIcon.filled: get bool
SampleFavoriteIcon.build: Widget(BuildContext context)
productL10n: const Map<String, Map<String, String>>
ProductStrings: final class
ProductStrings.new: const new(String language)
ProductStrings.of: factory new(BuildContext context)
ProductStrings.language: get String
ProductStrings.title: get String
ProductStrings.back: get String
ProductStrings.loading: get String
ProductStrings.photos: get String
ProductStrings.photoPage: String(int page, int count)
ProductStrings.photoUnavailable: get String
ProductStrings.openPhoto: get String
ProductStrings.closePhotos: get String
ProductStrings.rating: String(double rating, int count)
ProductStrings.ratingShown: String(double rating, int count)
ProductStrings.color: get String
ProductStrings.colorChosen: String(String color)
ProductStrings.size: get String
ProductStrings.chooseSize: get String
ProductStrings.quantity: get String
ProductStrings.decreaseQuantity: get String
ProductStrings.increaseQuantity: get String
ProductStrings.description: get String
ProductStrings.details: get String
ProductStrings.delivery: get String
ProductStrings.similar: get String
ProductStrings.addToCart: get String
ProductStrings.soldOut: get String
ProductStrings.soldOutMessage: get String
ProductStrings.favorite: get String
ProductStrings.favoriteAdded: get String
ProductStrings.favoriteRemoved: get String
ProductStrings.added: get String
ProductStrings.addFailed: get String
ProductStrings.addOffline: get String
ProductStrings.favoriteFailed: get String
ProductStrings.errorTitle: get String
ProductStrings.errorMessage: get String
ProductStrings.offlineTitle: get String
ProductStrings.offlineMessage: get String
ProductStrings.retry: get String
ProductStrings.wasPrice: String(String price)Strings
Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.
| Key | en | fr | ar |
|---|---|---|---|
productTitle | Product | Produit | المنتج |
productBack | Back | Retour | رجوع |
productLoading | Loading the product | Chargement du produit | جارٍ تحميل المنتج |
productPhotos | Photos | Photos | الصور |
productPhotoPage | Photo {page} of {count} | Photo {page} sur {count} | الصورة {page} من {count} |
productPhotoUnavailable | Photo unavailable | Photo indisponible | الصورة غير متاحة |
productOpenPhoto | Open full screen | Ouvrir en plein écran | فتح بملء الشاشة |
productClosePhotos | Close | Fermer | إغلاق |
productRating | {rating} out of 5 · {count} reviews | {rating} sur 5 · {count} avis | {rating} من 5 · التقييمات: {count} |
productRatingShown | {rating} · {count} reviews | {rating} · {count} avis | {rating} · التقييمات: {count} |
productDecimal | . | , | ٫ |
productColor | Color | Couleur | اللون |
productColorChosen | Color: {color} | Couleur : {color} | اللون: {color} |
productSize | Size | Taille | المقاس |
productChooseSize | Choose a size. | Choisissez une taille. | اختر مقاسًا. |
productQuantity | Quantity | Quantité | الكمية |
productDecreaseQuantity | Decrease quantity | Diminuer la quantité | إنقاص الكمية |
productIncreaseQuantity | Increase quantity | Augmenter la quantité | زيادة الكمية |
productDescription | Description | Description | الوصف |
productDetails | Materials and care | Composition et entretien | الخامات والعناية |
productDelivery | Delivery and returns | Livraison et retours | التوصيل والإرجاع |
productSimilar | You may also like | Vous aimerez aussi | قد يعجبك أيضًا |
productAddToCart | Add to cart | Ajouter au panier | أضف إلى السلة |
productSoldOut | Sold out | Épuisé | نفدت الكمية |
productSoldOutMessage | This product is no longer available. | Ce produit n’est plus disponible. | هذا المنتج لم يعد متاحًا. |
productFavorite | Favorite | Favori | المفضلة |
productFavoriteAdded | Added to favorites | Ajouté aux favoris | أضيف إلى المفضلة |
productFavoriteRemoved | Removed from favorites | Retiré des favoris | أزيل من المفضلة |
productAdded | Added to your cart. | Ajouté au panier. | أضيف إلى سلتك. |
productAddFailed | Couldn't add to your cart. Try again. | Ajout au panier impossible. Réessayez. | تعذّرت الإضافة إلى السلة. أعد المحاولة. |
productAddOffline | You are offline. Nothing was added. | Vous êtes hors ligne. Rien n’a été ajouté. | أنت غير متصل. لم تتم إضافة شيء. |
productFavoriteFailed | Couldn't update your favorites. | Impossible de mettre à jour vos favoris. | تعذّر تحديث المفضلة. |
productErrorTitle | This product could not load | Impossible de charger ce produit | تعذّر تحميل هذا المنتج |
productErrorMessage | Something went wrong on our side. | Un problème est survenu de notre côté. | حدثت مشكلة من جهتنا. |
productOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
productOfflineMessage | Check your connection, then try again. | Vérifiez votre connexion, puis réessayez. | تحقق من اتصالك ثم أعد المحاولة. |
productRetry | Try again | Réessayer | أعد المحاولة |
productWasPrice | Was {price} | Au lieu de {price} | بدلًا من {price} |
Reference renders
The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify product compares your copy to them.
















Use it to
- show the details of an item with its photos and price
- choose the size and color of a garment before buying it
- add an item to the bag or cart from its page
- open the photos of an item and enlarge them
- keep an item as a favorite for later
- Not for
- a product in a list of results: use a result card (result-card)
- a sequence of photos alone: use a page indicator (page-indicator)
- States