Fliin
On this page

Blocks / lists

Post card

Shows one post of a social feed: author, age, text, a gallery of one to four photos, a like toggle, comments and share with short counts, and the post menu. It reads as a single node.

16 of 16 variants pass
Post card, text state
Platform
Brightness
Text scale
Direction

Import

Post 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 post-card

Also installs: avatar, button, core, icons, menu, result-card, surface

Examples

Text

A text post with its author, time and counts. onLike toggles liked, and the counts are written by FlCountFormat: short on screen, exact for the screen reader. menu adds the post's own actions.

Post card, text state
import 'package:your_app/ui/fliin/components/menu/fl_menu.dart';
import 'package:your_app/ui/fliin/components/post_card/fl_post_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<PostCardTextExample> createState() => _PostCardTextExampleState();
}

class _PostCardTextExampleState extends State<PostCardTextExample> {
  var _liked = false;

  @override
  Widget build(BuildContext context) {
    const format = FlCountFormat();
    final likes = 1234 + (_liked == false ? 0 : (_liked ? 1 : -1));
    FlPostCount count(int value, String one, String many) => FlPostCount(
      text: value == 0 ? '' : format.short(value),
      label: '${format.exact(value)} ${value == 1 ? one : many}',
    );
    return FlPostCard(
      author: 'Leah Bernard',
      initials: 'LB',
      time: '5 min',
      text: 'First coffee on the terrace since the move. The street is slowly waking up.',
      likes: count(likes, 'like', 'likes'),
      comments: count(56, 'comment', 'comments'),
      liked: _liked,
      likeLabel: 'Like',
      onLike: () => setState(() => _liked = !_liked),
      commentLabel: 'Comment',
      onComment: () {},
      shareLabel: 'Share',
      onShare: () {},
      menu: [
        FlMenuItem(label: 'Hide', onPressed: () {}),
        FlMenuItem(label: 'Report', destructive: true, onPressed: () {}),
      ],
      menuLabel: 'More actions',
      onOpen: () {},
    );
  }
}

A post with a few photos: images lays them out in a grid, and imagesLabel names them for the screen reader.

Post card, gallery state
import 'package:your_app/ui/fliin/components/post_card/fl_post_card.dart';
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<PostCardGalleryExample> createState() => _PostCardGalleryExampleState();
}

class _PostCardGalleryExampleState extends State<PostCardGalleryExample> {
  var _liked = false;

  @override
  Widget build(BuildContext context) {
    const format = FlCountFormat();
    final likes = 48 + (_liked == false ? 0 : (_liked ? 1 : -1));
    FlPostCount count(int value, String one, String many) => FlPostCount(
      text: value == 0 ? '' : format.short(value),
      label: '${format.exact(value)} ${value == 1 ? one : many}',
    );
    return FlPostCard(
      author: 'Hugo Martin',
      initials: 'HM',
      time: '2 h',
      text: 'Two new pieces for the workshop.',
      images: [for (final art in const [FlSampleArt.mug, FlSampleArt.lamp, FlSampleArt.plant]) FlSampleImage(art)],
      imagesLabel: '3 photos',
      likes: count(likes, 'like', 'likes'),
      comments: count(3, 'comment', 'comments'),
      liked: _liked,
      likeLabel: 'Like',
      onLike: () => setState(() => _liked = !_liked),
      commentLabel: 'Comment',
      onComment: () {},
      shareLabel: 'Share',
      onShare: () {},
      onOpen: () {},
    );
  }
}

Liked

A post the user already likes: liked: true fills the heart. Photos alone, with no text, and a large count shortened to 12K.

Post card, liked state
import 'package:your_app/ui/fliin/components/post_card/fl_post_card.dart';
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<PostCardLikedExample> createState() => _PostCardLikedExampleState();
}

class _PostCardLikedExampleState extends State<PostCardLikedExample> {
  var _liked = true;

  @override
  Widget build(BuildContext context) {
    const format = FlCountFormat();
    final likes = 12408 + (_liked == true ? 0 : (_liked ? 1 : -1));
    FlPostCount count(int value, String one, String many) => FlPostCount(
      text: value == 0 ? '' : format.short(value),
      label: '${format.exact(value)} ${value == 1 ? one : many}',
    );
    return FlPostCard(
      author: 'Iris Moore',
      initials: 'IM',
      time: 'Yesterday',
      images: [for (final art in const [FlSampleArt.plant, FlSampleArt.bottle, FlSampleArt.bag, FlSampleArt.throwBlanket]) FlSampleImage(art)],
      imagesLabel: '4 photos',
      likes: count(likes, 'like', 'likes'),
      comments: count(0, 'comment', 'comments'),
      liked: _liked,
      likeLabel: 'Like',
      onLike: () => setState(() => _liked = !_liked),
      commentLabel: 'Comment',
      onComment: () {},
      shareLabel: 'Share',
      onShare: () {},
      onOpen: () {},
    );
  }
}

Card

A post in its own card (variant: card), for a feed among other content: the author's photo, a subtitle after the time, and a bookmark that onSave adds after the actions, filled in the accent once saved.

Post card, card state
import 'package:your_app/ui/fliin/components/post_card/fl_post_card.dart';
import 'package:your_app/ui/fliin/components/result_card/fl_result_card.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<PostCardCardExample> createState() => _PostCardCardExampleState();
}

class _PostCardCardExampleState extends State<PostCardCardExample> {
  var _liked = false;
  var _saved = true;

  @override
  Widget build(BuildContext context) {
    const format = FlCountFormat();
    final likes = 1204 + (_liked ? 1 : 0);
    return FlPostCard(
      variant: FlPostCardVariant.card,
      author: 'Kate Wilson',
      initials: 'KW',
      avatar: const AssetImage('images/people/1.jpg'),
      time: '3 h',
      subtitle: 'Lisbon',
      text: 'Golden hour over the Alfama roofs.',
      images: const [FlSampleImage(FlSampleArt.lamp)],
      imagesLabel: '1 photo',
      likes: FlPostCount(text: format.short(likes), label: '${format.exact(likes)} likes'),
      comments: const FlPostCount(text: '18', label: '18 comments'),
      liked: _liked,
      likeLabel: 'Like',
      onLike: () => setState(() => _liked = !_liked),
      commentLabel: 'Comment',
      onComment: () {},
      shareLabel: 'Share',
      onShare: () {},
      saved: _saved,
      saveLabel: 'Save',
      onSave: () => setState(() => _saved = !_saved),
      onOpen: () {},
    );
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: SingleChildScrollView(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlPostCard(
          author: "Leah Bernard",
          initials: "LB",
          time: (variant.dir == TextDirection.rtl ? "5 د" : "5 min"),
          text: (variant.dir == TextDirection.rtl
              ? "أول قهوة على الشرفة منذ الانتقال. الحي يستيقظ ببطء."
              : "First coffee on the terrace since the move. The neighborhood is slowly waking up."),
          likes: FlPostCount(
            text: (variant.dir == TextDirection.rtl ? "١٫٢ ألف" : "1.2K"),
            label: (variant.dir == TextDirection.rtl
                ? "الإعجابات: ١٬٢٣٤"
                : "1,234 likes"),
          ),
          comments: FlPostCount(
            text: (variant.dir == TextDirection.rtl ? "٥٦" : "56"),
            label: (variant.dir == TextDirection.rtl
                ? "التعليقات: ٥٦"
                : "56 comments"),
          ),
          liked: false,
          likeLabel: (variant.dir == TextDirection.rtl ? "إعجاب" : "Like"),
          onLike: () {},
          commentLabel: (variant.dir == TextDirection.rtl
              ? "تعليق"
              : "Comment"),
          onComment: () {},
          shareLabel: (variant.dir == TextDirection.rtl ? "مشاركة" : "Share"),
          onShare: () {},
          menu: [
            FlMenuItem(
              label: (variant.dir == TextDirection.rtl ? "إخفاء" : "Hide"),
              onPressed: () {},
            ),
            FlMenuItem(
              label: (variant.dir == TextDirection.rtl ? "إبلاغ" : "Report"),
              destructive: true,
              onPressed: () {},
            ),
          ],
          menuLabel: (variant.dir == TextDirection.rtl
              ? "مزيد من الإجراءات"
              : "More actions"),
          onOpen: () {},
        ),
        FlPostCard(
          author: "Hugo Martin",
          initials: "HM",
          avatarColor: FlAvatarColor.success,
          time: (variant.dir == TextDirection.rtl ? "2 س" : "2 h"),
          text: (variant.dir == TextDirection.rtl
              ? "قطعتان جديدتان للمشغل."
              : "Two new pieces for the studio."),
          images: const [
            FlSampleImage(FlSampleArt.mug),
            FlSampleImage(FlSampleArt.lamp),
          ],
          imagesLabel: (variant.dir == TextDirection.rtl
              ? "الصور: ٢"
              : "2 photos"),
          likes: FlPostCount(
            text: (variant.dir == TextDirection.rtl ? "٤٨" : "48"),
            label: (variant.dir == TextDirection.rtl
                ? "الإعجابات: ٤٨"
                : "48 likes"),
          ),
          comments: FlPostCount(
            text: (variant.dir == TextDirection.rtl ? "٣" : "3"),
            label: (variant.dir == TextDirection.rtl
                ? "التعليقات: ٣"
                : "3 comments"),
          ),
          liked: true,
          likeLabel: (variant.dir == TextDirection.rtl ? "إعجاب" : "Like"),
          onLike: () {},
          commentLabel: (variant.dir == TextDirection.rtl
              ? "تعليق"
              : "Comment"),
          onComment: () {},
          shareLabel: (variant.dir == TextDirection.rtl ? "مشاركة" : "Share"),
          onShare: () {},
          onOpen: () {},
        ),
        FlPostCard(
          author: "Iris Moore",
          initials: "IM",
          avatarColor: FlAvatarColor.warning,
          time: (variant.dir == TextDirection.rtl ? "أمس" : "Yesterday"),
          images: const [
            FlSampleImage(FlSampleArt.plant),
            FlSampleImage(FlSampleArt.bottle),
            FlSampleImage(FlSampleArt.bag),
            FlSampleImage(FlSampleArt.throwBlanket),
          ],
          imagesLabel: (variant.dir == TextDirection.rtl
              ? "الصور: ٤"
              : "4 photos"),
          likes: FlPostCount(
            text: (variant.dir == TextDirection.rtl ? "١٢ ألف" : "12K"),
            label: (variant.dir == TextDirection.rtl
                ? "الإعجابات: ١٢٬٤٠٨"
                : "12,408 likes"),
          ),
          comments: FlPostCount(
            text: '',
            label: (variant.dir == TextDirection.rtl
                ? "التعليقات: ٠"
                : "0 comments"),
          ),
          liked: false,
          likeLabel: (variant.dir == TextDirection.rtl ? "إعجاب" : "Like"),
          onLike: () {},
          commentLabel: (variant.dir == TextDirection.rtl
              ? "تعليق"
              : "Comment"),
          onComment: () {},
          shareLabel: (variant.dir == TextDirection.rtl ? "مشاركة" : "Share"),
          onShare: () {},
          onOpen: () {},
        ),
      ],
    ),
  ),
);

API

FlPostCardVariant: enum {flat, card}
FlPostCardVariant.flat: static final FlPostCardVariant
FlPostCardVariant.card: static final FlPostCardVariant
FlCountFormat: final class
FlCountFormat.new: const new({String decimal, String group, String thousands, String millions, String billions, String digits})
FlCountFormat.decimal: get String
FlCountFormat.group: get String
FlCountFormat.thousands: get String
FlCountFormat.millions: get String
FlCountFormat.billions: get String
FlCountFormat.digits: get String
FlCountFormat.exact: String(int count)
FlCountFormat.short: String(int count)
FlPostAge: sealed class permits {FlAgeDate, FlAgeDays, FlAgeHours, FlAgeMinutes, FlAgeNow}
FlPostAge.new: const new()
FlPostAge.between: factory new(DateTime at, DateTime now)
FlAgeNow: final class extends FlPostAge
FlAgeNow.new: const new()
FlAgeMinutes: final class extends FlPostAge
FlAgeMinutes.new: const new(int minutes)
FlAgeMinutes.minutes: get int
FlAgeHours: final class extends FlPostAge
FlAgeHours.new: const new(int hours)
FlAgeHours.hours: get int
FlAgeDays: final class extends FlPostAge
FlAgeDays.new: const new(int days)
FlAgeDays.days: get int
FlAgeDate: final class extends FlPostAge
FlAgeDate.new: const new(DateTime at)
FlAgeDate.at: get DateTime
FlPostCount: final class
FlPostCount.new: const new({required String text, required String label})
FlPostCount.text: get String
FlPostCount.label: get String
FlPostGlyph: enum {like, liked, comment, share, more, check}
FlPostGlyph.like: static final FlPostGlyph
FlPostGlyph.liked: static final FlPostGlyph
FlPostGlyph.comment: static final FlPostGlyph
FlPostGlyph.share: static final FlPostGlyph
FlPostGlyph.more: static final FlPostGlyph
FlPostGlyph.check: static final FlPostGlyph
FlPostIcon: class extends StatelessWidget
FlPostIcon.new: const new(FlPostGlyph glyph, {super.key, Color? color})
FlPostIcon.glyph: get FlPostGlyph
FlPostIcon.color: get Color?
FlPostIcon.build: Widget(BuildContext context)
FlPostCard: class extends StatelessWidget
FlPostCard.new: const new({super.key, required String author, required String initials, ImageProvider? avatar, FlAvatarColor avatarColor, FlAvatarVariant avatarVariant, required String time, String text, List<ImageProvider> images, String? imagesLabel, required FlPostCount likes, required FlPostCount comments, required bool liked, required String likeLabel, required VoidCallback? onLike, required String commentLabel, required VoidCallback? onComment, String? shareLabel, VoidCallback? onShare, List<FlMenuEntry> menu, String? menuLabel, VoidCallback? onOpen, VoidCallback? onAuthor, String? authorActionLabel, int? maxLines, String? subtitle, bool saved, String? saveLabel, VoidCallback? onSave, FlPostCardVariant variant})
FlPostCard.author: get String
FlPostCard.initials: get String
FlPostCard.avatar: get ImageProvider?
FlPostCard.avatarColor: get FlAvatarColor
FlPostCard.avatarVariant: get FlAvatarVariant
FlPostCard.time: get String
FlPostCard.text: get String
FlPostCard.images: get List<ImageProvider>
FlPostCard.imagesLabel: get String?
FlPostCard.likes: get FlPostCount
FlPostCard.comments: get FlPostCount
FlPostCard.liked: get bool
FlPostCard.likeLabel: get String
FlPostCard.onLike: get VoidCallback?
FlPostCard.commentLabel: get String
FlPostCard.onComment: get VoidCallback?
FlPostCard.shareLabel: get String?
FlPostCard.onShare: get VoidCallback?
FlPostCard.menu: get List<FlMenuEntry>
FlPostCard.menuLabel: get String?
FlPostCard.onOpen: get VoidCallback?
FlPostCard.onAuthor: get VoidCallback?
FlPostCard.authorActionLabel: get String?
FlPostCard.maxLines: get int?
FlPostCard.subtitle: get String?
FlPostCard.saved: get bool
FlPostCard.saveLabel: get String?
FlPostCard.onSave: get VoidCallback?
FlPostCard.variant: get FlPostCardVariant
FlPostCard.build: Widget(BuildContext context)

Reference renders

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

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

Use it to

  • show a social post with its author, text and images
  • toggle the like of a post with a tap on the heart
  • show short like and comment counters such as 1.2K
  • present one to four images of a post as a gallery
  • hide or report a post from its menu
Not for
  • a product with its price: use a result card (result-card)
  • a message in a discussion: use a message bubble (message-bubble)
States
  • Text
  • Gallery
  • Liked

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