Fliin
On this page

Blocks / commerce

Result card

Presents a search result or a product with its thumbnail, name, price and a rating or delivery line, as a list row or a grid tile, and opens it on tap.

16 of 16 variants pass
Result card, row state
Platform
Brightness
Text scale
Direction

Import

Result card 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 result-card

Also installs: core, icons, skeleton, surface

Examples

Row

A search result as a row: the picture, the title and seller, the price and the rating. The whole card is one button.

Result card, row state
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:flutter/widgets.dart';

class ResultCardRowExample extends StatelessWidget {
  const ResultCardRowExample({super.key});

  @override
  Widget build(BuildContext context) {
    return FlResultCard(
      title: 'Linen tote bag',
      subtitle: 'Brume Studio',
      price: '€49.00',
      meta: '4.6 · 128 reviews',
      media: const FlResultImage(image: FlSampleImage(FlSampleArt.bag)),
      onPressed: () {},
    );
  }
}

Tile

FlResultCardLayout.tile stacks the picture above the text, for results in a grid.

Result card, tile state
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

class ResultCardTileExample extends StatelessWidget {
  const ResultCardTileExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: FlScope.of(context).space.md,
      children: [
        Expanded(
          child: FlResultCard(
            layout: FlResultCardLayout.tile,
            title: 'Wool throw',
            price: '€79.00',
            media: const FlResultImage(image: FlSampleImage(FlSampleArt.throwBlanket)),
            onPressed: () {},
          ),
        ),
        Expanded(
          child: FlResultCard(
            layout: FlResultCardLayout.tile,
            title: 'Stoneware mug',
            subtitle: 'Set of 2',
            price: '€32.00',
            media: const FlResultImage(image: FlSampleImage(FlSampleArt.mug)),
            onPressed: () {},
          ),
        ),
      ],
    );
  }
}

Sale

A product on sale: badge on the image, the price before the discount in originalPrice, and on a tile a favorite button in action, which keeps its own touch.

Result card, sale state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ResultCardSaleExample> createState() => _ResultCardSaleExampleState();
}

class _ResultCardSaleExampleState extends State<ResultCardSaleExample> {
  var _saved = false;

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: space.md,
      children: [
        FlResultCard(
          title: 'Linen tote bag',
          subtitle: 'Brume Studio',
          price: '€39.20',
          originalPrice: '€49.00',
          originalPriceSemanticLabel: 'was €49.00',
          meta: 'Delivered tomorrow',
          media: const FlResultImage(image: FlSampleImage(FlSampleArt.bag)),
          onPressed: () {},
        ),
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          spacing: space.md,
          children: [
            Expanded(
              child: FlResultCard(
                layout: FlResultCardLayout.tile,
                title: 'Wool throw',
                price: '€63.20',
                originalPrice: '€79.00',
                originalPriceSemanticLabel: 'was €79.00',
                badge: const FlChip(label: '−20%', color: FlChipColor.danger, size: FlChipSize.sm),
                action: FlButton.icon(
                  label: _saved ? 'Remove from favorites' : 'Add to favorites',
                  icon: FlIcon(_saved ? FlIcons.heartFill : FlIcons.heart),
                  variant: FlButtonVariant.secondary,
                  size: FlButtonSize.sm,
                  selected: _saved,
                  onPressed: () => setState(() => _saved = !_saved),
                ),
                media: const FlResultImage(image: FlSampleImage(FlSampleArt.throwBlanket)),
                onPressed: () {},
              ),
            ),
            Expanded(
              child: FlResultCard(
                layout: FlResultCardLayout.tile,
                title: 'Potted plant',
                price: '€24.00',
                badge: const FlChip(label: 'New', variant: FlChipVariant.secondary, size: FlChipSize.sm),
                media: const FlResultImage(image: FlSampleImage(FlSampleArt.plant)),
                onPressed: () {},
              ),
            ),
          ],
        ),
      ],
    );
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: SingleChildScrollView(
    padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: FlScope.of(context).space.md,
      children: [
        FlResultCard(
          title: (variant.dir == TextDirection.rtl
              ? "حقيبة كتان"
              : "Linen tote bag"),
          subtitle: (variant.dir == TextDirection.rtl
              ? "أتيليه بروم"
              : "Brume Studio"),
          price: (variant.dir == TextDirection.rtl ? "49٫00 €" : "€49.00"),
          rating: (variant.dir == TextDirection.rtl ? "4٫6" : "4.6"),
          ratingSemanticLabel: (variant.dir == TextDirection.rtl
              ? "التقييم ٤٫٦ من ٥"
              : "Rated 4.6 out of 5"),
          meta: (variant.dir == TextDirection.rtl
              ? "128 تقييمًا"
              : "128 reviews"),
          media: FlResultImage(image: const FlSampleImage(FlSampleArt.bag)),
          onPressed: () {},
        ),
        FlResultCard(
          title: (variant.dir == TextDirection.rtl
              ? "قارورة حافظة للحرارة"
              : "Insulated bottle"),
          price: (variant.dir == TextDirection.rtl ? "24٫90 €" : "€24.90"),
          meta: (variant.dir == TextDirection.rtl
              ? "التوصيل غدًا"
              : "Delivered tomorrow"),
        ),
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          spacing: FlScope.of(context).space.md,
          children: [
            Expanded(
              child: FlResultCard(
                layout: FlResultCardLayout.tile,
                title: (variant.dir == TextDirection.rtl
                    ? "بطانية صوف"
                    : "Wool throw"),
                price: (variant.dir == TextDirection.rtl
                    ? "79٫00 €"
                    : "€79.00"),
                media: FlResultImage(
                  image: const FlSampleImage(FlSampleArt.throwBlanket),
                ),
                onPressed: () {},
              ),
            ),
            Expanded(
              child: FlResultCard(
                layout: FlResultCardLayout.tile,
                title: (variant.dir == TextDirection.rtl
                    ? "كوب فخاري"
                    : "Stoneware mug"),
                subtitle: (variant.dir == TextDirection.rtl
                    ? "طقم من 2"
                    : "Set of 2"),
                price: (variant.dir == TextDirection.rtl
                    ? "32٫00 €"
                    : "€32.00"),
                media: FlResultImage(
                  image: const FlSampleImage(FlSampleArt.mug),
                ),
                onPressed: () {},
              ),
            ),
          ],
        ),
      ],
    ),
  ),
);

API

FlResultCardLayout: enum {row, tile}
FlResultCardLayout.row: static final FlResultCardLayout
FlResultCardLayout.tile: static final FlResultCardLayout
FlResultCard: class extends StatelessWidget
FlResultCard.new: const new({super.key, required String title, String? subtitle, String? price, String? meta, Widget? media, FlResultCardLayout layout, VoidCallback? onPressed, String? rating, String? ratingSemanticLabel, String? originalPrice, String? originalPriceSemanticLabel, Widget? badge, Widget? action})
FlResultCard.originalPrice: get String?
FlResultCard.originalPriceSemanticLabel: get String?
FlResultCard.badge: get Widget?
FlResultCard.action: get Widget?
FlResultCard.rating: get String?
FlResultCard.ratingSemanticLabel: get String?
FlResultCard.title: get String
FlResultCard.subtitle: get String?
FlResultCard.price: get String?
FlResultCard.meta: get String?
FlResultCard.media: get Widget?
FlResultCard.layout: get FlResultCardLayout
FlResultCard.onPressed: get VoidCallback?
FlResultCard.build: Widget(BuildContext context)
FlResultImage: class extends StatelessWidget
FlResultImage.new: const new({super.key, required ImageProvider image, BoxFit fit, Widget? error})
FlResultImage.image: get ImageProvider
FlResultImage.fit: get BoxFit
FlResultImage.error: get Widget?
FlResultImage.build: Widget(BuildContext context)
FlSampleArt: enum {bag, bottle, mug, throwBlanket, lamp, plant}
FlSampleArt.bag: static final FlSampleArt
FlSampleArt.bottle: static final FlSampleArt
FlSampleArt.mug: static final FlSampleArt
FlSampleArt.throwBlanket: static final FlSampleArt
FlSampleArt.lamp: static final FlSampleArt
FlSampleArt.plant: static final FlSampleArt
FlSampleImage: final class extends ImageProvider<FlSampleImage>
FlSampleImage.new: const new(FlSampleArt art)
FlSampleImage.art: get FlSampleArt
FlSampleImage.obtainKey: Future<FlSampleImage>(ImageConfiguration configuration)
FlSampleImage.loadImage: ImageStreamCompleter(FlSampleImage key, ImageDecoderCallback decode)
FlSampleImage.operator ==: bool(Object other)
FlSampleImage.hashCode: get int

Reference renders

The 16 images published with version 0.2.3, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify result-card compares your copy to them.

iOS · Light
Result card, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Result card, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Result card, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Result card, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Result card, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Result card, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Result card, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Result card, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Result card, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Result card, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Result card, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Result card, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Result card, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Result card, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Result card, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Result card, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • present a product with its thumbnail, name and price
  • show a list of products as cards
  • make a two-column grid of product tiles
  • show the rating and review count under the price
  • open a product page from its card
Not for
  • a settings or navigation row: use a list row (list-row)
  • free content on a surface: use a card (card)
States
  • Row
  • Tile

Arrows to move, Enter to open, Escape to close.