Social20 views

How to Build a Suggested Follows Onboarding Screen in Flutter (Full Code + Preview)

A brand-new social account has an empty feed, and an empty feed is where most sign-ups quietly die. This tutorial builds Pulse's fifth onboarding step in Flutter: a list of seven recommended accounts, each with a `_Monogram` avatar that derives initials from the name, a follower count, a reason line like '3 mutual follows', and a `_FollowButton` that flips between solid indigo and a muted outline. A single `Set<String>` of handles drives every row, the 'Follow all' / 'Clear' shortcut, and a pinned footer that reads 'Skip for now' or 'Continue · Following N'. Pure Flutter, no packages.

Pulse · Suggested Follows — Social Flutter UI screen
Live preview — Pulse · Suggested Follows, built in pure Flutter.

What you'll build

  • A `Set<String>`-backed `_following` state where per-row taps, 'Follow all' and the footer count can never disagree
  • A `_Monogram` circle that computes initials with a `RegExp(r'\s+')` split and tints itself with each account's accent colour at 18% fill and 50% border
  • A `_FollowButton` pill that swaps `Material` colour, border and text colour between 'Follow' and 'Following' in one ternary set
  • A `_SetupHeader` whose eight-segment progress bar is generated with `List<Widget>.generate` and filled up to `step`
  • A `_SetupFooter` `FilledButton` whose label changes from 'Skip for now' at zero to 'Continue · Following N' as the count grows

Step-by-step build

1

Create the file

Add a new file at lib/social_setup_follow/social_setup_follow_screen.dart in your Flutter project.

2

Register the bundled fonts

No external packages — this is pure Flutter. It does bundle its design font (Inter), so drop the font file into fonts/ and declare it in pubspec.yaml:

pubspec.yaml
flutter:
  fonts:
    - family: Inter
      fonts:
        - asset: fonts/Inter-Regular.ttf
3

Build it, piece by piece

Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.

The screen shell, its palette and the account model

social_setup_follow_screen.dart
import 'package:flutter/material.dart';

/// Suggested Follows — fifth step of Pulse profile setup. A list of recommended
/// accounts (painted monogram avatar, name, handle, follower count, and the
/// reason they’re suggested) each carry a Follow / Following toggle, and a
/// "Follow all" shortcut sits up top. The pinned CTA reflects how many the user
/// has followed. Self-contained per CONVENTIONS.md: pure Flutter, bundled Inter
/// font, own dark theme, SafeArea.
class SocialSetupFollowScreen extends StatefulWidget {
  const SocialSetupFollowScreen({
    super.key,
    this.onBack,
    this.onContinue,
  });

  final VoidCallback? onBack;
  final VoidCallback? onContinue;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0B0F);
  static const Color _surface = Color(0xFF15151B);
  static const Color _surfaceAlt = Color(0xFF1D1D26);
  static const Color _brand = Color(0xFF6E56F7);
  static const Color _accent = Color(0xFF9B8CFF);
  static const Color _hairline = Color(0xFF26262F);
  static const Color _textHi = Color(0xFFF4F4F7);
  static const Color _textLo = Color(0xFFB5B5C2);
  static const Color _muted = Color(0xFF8A8A99);

  @override
  State<SocialSetupFollowScreen> createState() =>
      _SocialSetupFollowScreenState();
}

class _Account {
  const _Account(this.name, this.handle, this.followers, this.reason, this.color);
  final String name;
  final String handle;
  final String followers;
  final String reason;
  final Color color;
}

`SocialSetupFollowScreen` is a `StatefulWidget` because, unlike a static onboarding page, it has to remember which accounts the user has tapped. It takes only two callbacks, `onBack` and `onContinue`, so the flow controller outside decides where step 5 of 8 goes next. Nine colour constants define a near-monochrome dark palette: `_bg` #0B0B0F, two surfaces (#15151B and #1D1D26), a #26262F `_hairline`, and three text tiers from #F4F4F7 down to #8A8A99. The only saturated colours are `_brand` #6E56F7 and the lighter `_accent` #9B8CFF, which is why the Follow pills and progress bar read as the focal points. `_Account` is a tiny immutable model with `name`, `handle`, `followers`, `reason` and a per-account `Color`; storing the colour on the model, rather than deriving it, is what lets each monogram carry its own tint.

Seven seeded accounts and a Set of followed handles

social_setup_follow_screen.dart
class _SocialSetupFollowScreenState extends State<SocialSetupFollowScreen> {
  static const List<_Account> _accounts = <_Account>[
    _Account('Maya Chen', 'mayadraws', '128K', 'Popular in Design',
        Color(0xFF9B8CFF)),
    _Account('The Verge', 'verge', '3.2M', 'Popular in Technology',
        Color(0xFF6E56F7)),
    _Account('Jordan Blake', 'jblake', '54K', '3 mutual follows',
        Color(0xFF34D399)),
    _Account('Lens & Light', 'lenslight', '210K', 'Popular in Photography',
        Color(0xFFF4476B)),
    _Account('Priya Nair', 'priyacodes', '88K', 'Suggested for you',
        Color(0xFF38BDF8)),
    _Account('Studio Nord', 'studionord', '17K', 'Popular in Design',
        Color(0xFFFBBF24)),
    _Account('Sam Ortiz', 'samshoots', '41K', '5 mutual follows',
        Color(0xFFEC4899)),
  ];

  late final Set<String> _following = <String>{'mayadraws', 'verge'};

The suggestions are a `static const List<_Account>` of seven entries with varied follower strings ('128K', '3.2M', '17K') and reasons that mix topic popularity ('Popular in Design') with social proof ('3 mutual follows', '5 mutual follows'). Each carries a distinct accent — mint #34D399, rose #F4476B, sky #38BDF8, amber #FBBF24, pink #EC4899 — so the column of avatars does not look like a single-colour list. The crucial line is `late final Set<String> _following = <String>{'mayadraws', 'verge'}`: state is a Set of handles, not a list of booleans indexed by position. Two accounts start pre-followed so the footer already reads 'Continue · Following 2' on first paint. Because a Set has no duplicates and `contains` is O(1), every row, the header shortcut and the footer can all ask the same object the same question without any bookkeeping.

Forcing a dark theme, the title row and the Follow all / Clear toggle

social_setup_follow_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: SocialSetupFollowScreen._bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _SetupHeader(step: 5, total: 8, onBack: widget.onBack),
              Padding(
                padding: const EdgeInsets.fromLTRB(24, 16, 24, 4),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    const Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text(
                            'Follow a few accounts',
                            style: TextStyle(
                              fontFamily: SocialSetupFollowScreen._font,
                              fontSize: 27,
                              fontWeight: FontWeight.w700,
                              letterSpacing: -0.7,
                              color: SocialSetupFollowScreen._textHi,
                            ),
                          ),
                          SizedBox(height: 8),
                          Text(
                            'Your feed comes alive once you follow people.',
                            style: TextStyle(
                              fontFamily: SocialSetupFollowScreen._font,
                              fontSize: 14.5,
                              height: 1.4,
                              color: SocialSetupFollowScreen._textLo,
                            ),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(width: 12),
                    GestureDetector(
                      onTap: () => setState(() {
                        if (_following.length == _accounts.length) {
                          _following.clear();
                        } else {
                          _following.addAll(
                              _accounts.map((_Account a) => a.handle));
                        }
                      }),
                      child: Padding(
                        padding: const EdgeInsets.only(top: 4),
                        child: Text(
                          _following.length == _accounts.length
                              ? 'Clear'
                              : 'Follow all',
                          style: const TextStyle(
                            fontFamily: SocialSetupFollowScreen._font,
                            fontSize: 14,
                            fontWeight: FontWeight.w600,
                            color: SocialSetupFollowScreen._accent,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),

`build` wraps everything in `Theme(data: ThemeData.dark(useMaterial3: true))` so the screen ignores whatever light theme the host app uses, then lays out header, title, list and footer in a `Column` inside `SafeArea`. The title 'Follow a few accounts' is 27px, weight 700, with `letterSpacing: -0.7` to tighten the large glyphs, above a 14.5px `_textLo` subtitle. To its right, a `GestureDetector` around plain text acts as the bulk shortcut. Its `onTap` checks `_following.length == _accounts.length`: if everyone is followed it calls `_following.clear()`, otherwise `addAll(_accounts.map((a) => a.handle))`. The same comparison picks the label, so the text reads 'Follow all' until the last account is followed and then flips to 'Clear'. There is no separate `_allFollowed` boolean to keep in sync; the Set's length is the truth.

The account list and per-row toggling

social_setup_follow_screen.dart
              Expanded(
                child: ListView.separated(
                  padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
                  itemCount: _accounts.length,
                  separatorBuilder: (BuildContext context, int i) =>
                      const SizedBox(height: 12),
                  itemBuilder: (BuildContext context, int i) {
                    final _Account a = _accounts[i];
                    final bool following = _following.contains(a.handle);
                    return _AccountRow(
                      account: a,
                      following: following,
                      onToggle: () => setState(() {
                        if (following) {
                          _following.remove(a.handle);
                        } else {
                          _following.add(a.handle);
                        }
                      }),
                    );
                  },
                ),
              ),
              _SetupFooter(
                count: _following.length,
                onContinue: widget.onContinue,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The list is a `ListView.separated` inside `Expanded`, so it takes whatever height remains between title and footer and scrolls independently. `separatorBuilder` returns a 12px `SizedBox`, which is cleaner than giving each card a bottom margin because the last card gets no trailing gap. In `itemBuilder`, `final bool following = _following.contains(a.handle)` is computed once per row and passed down along with the `_Account`. The `onToggle` closure captures that boolean: if the row was already followed it calls `_following.remove(a.handle)`, otherwise `add`. Both branches run inside `setState`, so the rebuild also refreshes the header shortcut label and the footer count. Finally `_SetupFooter` receives `count: _following.length`, meaning the CTA never stores its own number, it just reads the Set on every build.

The account card: monogram, name, follower count and reason

social_setup_follow_screen.dart
class _AccountRow extends StatelessWidget {
  const _AccountRow({
    required this.account,
    required this.following,
    this.onToggle,
  });
  final _Account account;
  final bool following;
  final VoidCallback? onToggle;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: SocialSetupFollowScreen._surface,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: SocialSetupFollowScreen._hairline),
      ),
      child: Row(
        children: <Widget>[
          _Monogram(name: account.name, color: account.color, size: 46),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  children: <Widget>[
                    Flexible(
                      child: Text(
                        account.name,
                        overflow: TextOverflow.ellipsis,
                        style: const TextStyle(
                          fontFamily: SocialSetupFollowScreen._font,
                          fontSize: 15,
                          fontWeight: FontWeight.w600,
                          color: SocialSetupFollowScreen._textHi,
                        ),
                      ),
                    ),
                    const SizedBox(width: 6),
                    Text(
                      '· ${account.followers}',
                      style: const TextStyle(
                        fontFamily: SocialSetupFollowScreen._font,
                        fontSize: 12.5,
                        fontWeight: FontWeight.w500,
                        color: SocialSetupFollowScreen._muted,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 2),
                Text(
                  account.reason,
                  style: const TextStyle(
                    fontFamily: SocialSetupFollowScreen._font,
                    fontSize: 12.5,
                    color: SocialSetupFollowScreen._muted,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(width: 10),
          _FollowButton(following: following, onTap: onToggle),
        ],
      ),
    );
  }
}

`_AccountRow` is a stateless card with 12px padding, `_surface` fill, 16px radius and a `_hairline` border, so it separates from the #0B0B0F background by a single tint step rather than a shadow. A `Row` places a 46px `_Monogram` on the left, then an `Expanded` text column, then the follow pill. The name row is worth studying: the name is wrapped in `Flexible` with `overflow: TextOverflow.ellipsis`, while the `'· ${account.followers}'` text sits outside it unconstrained. That means a long name like 'Lens & Light' truncates but the follower count is never pushed off the card. Name is 15px weight 600 in `_textHi`; both the follower count and the reason line drop to 12.5px `_muted`, with only a 2px gap between the name row and the reason, so the card reads as one headline and one quiet caption.

One button, two states: the Follow / Following pill

social_setup_follow_screen.dart
class _FollowButton extends StatelessWidget {
  const _FollowButton({required this.following, this.onTap});
  final bool following;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: following
          ? SocialSetupFollowScreen._surfaceAlt
          : SocialSetupFollowScreen._brand,
      borderRadius: BorderRadius.circular(11),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(11),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(11),
            border: Border.all(
              color: following
                  ? SocialSetupFollowScreen._hairline
                  : SocialSetupFollowScreen._brand,
            ),
          ),
          child: Text(
            following ? 'Following' : 'Follow',
            style: TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 13.5,
              fontWeight: FontWeight.w600,
              color: following ? SocialSetupFollowScreen._textLo : Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

`_FollowButton` uses `Material` plus `InkWell` rather than a `TextButton` so it gets a ripple clipped to an 11px radius while keeping full control of colour. Every visual property is a ternary on `following`: the `Material` colour is `_brand` indigo when not followed and `_surfaceAlt` #1D1D26 when followed; the `Border.all` colour matches `_brand` in the first case and drops to `_hairline` in the second; the label swaps 'Follow' for 'Following'; and the text goes from `Colors.white` to `_textLo`. The result is that the un-followed state is the loud, filled action and the followed state visually recedes into the card, inviting the eye to the next un-followed row. Padding is 16px by 9px with 13.5px weight-600 text, small enough that seven pills stacked in a column do not compete with the footer CTA.

Monogram avatars from initials, no image assets

social_setup_follow_screen.dart
class _Monogram extends StatelessWidget {
  const _Monogram({required this.name, required this.color, required this.size});
  final String name;
  final Color color;
  final double size;

  String get _initials {
    final List<String> parts = name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String s) => s.isNotEmpty)
        .toList();
    if (parts.isEmpty) return '?';
    if (parts.length == 1) return parts.first[0].toUpperCase();
    return (parts.first[0] + parts.last[0]).toUpperCase();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: color.withValues(alpha: 0.18),
        border: Border.all(color: color.withValues(alpha: 0.5)),
      ),
      child: Center(
        child: Text(
          _initials,
          style: TextStyle(
            fontFamily: SocialSetupFollowScreen._font,
            fontSize: size * 0.36,
            fontWeight: FontWeight.w700,
            color: color,
          ),
        ),
      ),
    );
  }
}

`_Monogram` replaces network avatars with a computed one, which is why the screen has zero image dependencies. The `_initials` getter trims the name, splits on `RegExp(r'\s+')`, filters empty parts, and returns '?' for an empty string, the single upper-cased first letter for a one-word name, or first-plus-last initials otherwise — so 'The Verge' becomes 'TV' and 'Lens & Light' becomes 'LL'. The circle is a `BoxShape.circle` container whose fill is the account colour at `withValues(alpha: 0.18)` and whose 1px border is the same colour at 0.5 alpha, with the letters drawn in the fully opaque colour. Font size is `size * 0.36`, so the same widget scales correctly if you pass 32 or 64 instead of 46. Three alpha levels of one hue give each avatar depth without a gradient or shadow.

Progress header and the count-aware footer CTA

social_setup_follow_screen.dart
class _SetupHeader extends StatelessWidget {
  const _SetupHeader({required this.step, required this.total, this.onBack});
  final int step;
  final int total;
  final VoidCallback? onBack;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 8, 24, 4),
      child: Row(
        children: <Widget>[
          IconButton(
            onPressed: onBack,
            icon: const Icon(Icons.arrow_back_ios_new,
                size: 18, color: SocialSetupFollowScreen._textHi),
          ),
          Expanded(
            child: Row(
              children: List<Widget>.generate(total, (int i) {
                return Expanded(
                  child: Container(
                    height: 4,
                    margin: const EdgeInsets.symmetric(horizontal: 3),
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(2),
                      color: i < step
                          ? SocialSetupFollowScreen._brand
                          : SocialSetupFollowScreen._hairline,
                    ),
                  ),
                );
              }),
            ),
          ),
          const SizedBox(width: 12),
          Text(
            '$step/$total',
            style: const TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 12.5,
              fontWeight: FontWeight.w600,
              color: SocialSetupFollowScreen._muted,
            ),
          ),
        ],
      ),
    );
  }
}

class _SetupFooter extends StatelessWidget {
  const _SetupFooter({required this.count, this.onContinue});
  final int count;
  final VoidCallback? onContinue;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: SocialSetupFollowScreen._bg,
        border:
            Border(top: BorderSide(color: SocialSetupFollowScreen._hairline)),
      ),
      padding: const EdgeInsets.fromLTRB(24, 14, 24, 16),
      child: SizedBox(
        width: double.infinity,
        height: 54,
        child: FilledButton(
          onPressed: onContinue,
          style: FilledButton.styleFrom(
            backgroundColor: SocialSetupFollowScreen._brand,
            foregroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(15),
            ),
          ),
          child: Text(
            count == 0 ? 'Skip for now' : 'Continue · Following $count',
            style: const TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 16,
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      ),
    );
  }
}

`_SetupHeader` puts a back `IconButton` (an 18px `arrow_back_ios_new`) beside a progress bar built from `List<Widget>.generate(total, ...)`: each segment is an `Expanded` 4px-tall container with 3px horizontal margins, coloured `_brand` when `i < step` and `_hairline` otherwise, so passing `step: 5, total: 8` lights exactly five of eight bars. A '5/8' label in 12.5px `_muted` confirms it numerically. `_SetupFooter` pins a 54px full-width `FilledButton` under a top hairline, with `_brand` background and a 15px rounded shape. Its label is the screen's honesty check: `count == 0 ? 'Skip for now' : 'Continue · Following $count'`. A user who followed nobody is offered a skip rather than a misleading 'Continue', and one who followed people sees the number they chose, so the CTA doubles as a live summary of the Set above it.

Full code

The complete, ready-to-paste source. Free to use in your projects — one click copies it all.

import 'package:flutter/material.dart';

/// Suggested Follows — fifth step of Pulse profile setup. A list of recommended
/// accounts (painted monogram avatar, name, handle, follower count, and the
/// reason they’re suggested) each carry a Follow / Following toggle, and a
/// "Follow all" shortcut sits up top. The pinned CTA reflects how many the user
/// has followed. Self-contained per CONVENTIONS.md: pure Flutter, bundled Inter
/// font, own dark theme, SafeArea.
class SocialSetupFollowScreen extends StatefulWidget {
  const SocialSetupFollowScreen({
    super.key,
    this.onBack,
    this.onContinue,
  });

  final VoidCallback? onBack;
  final VoidCallback? onContinue;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0B0F);
  static const Color _surface = Color(0xFF15151B);
  static const Color _surfaceAlt = Color(0xFF1D1D26);
  static const Color _brand = Color(0xFF6E56F7);
  static const Color _accent = Color(0xFF9B8CFF);
  static const Color _hairline = Color(0xFF26262F);
  static const Color _textHi = Color(0xFFF4F4F7);
  static const Color _textLo = Color(0xFFB5B5C2);
  static const Color _muted = Color(0xFF8A8A99);

  @override
  State<SocialSetupFollowScreen> createState() =>
      _SocialSetupFollowScreenState();
}

class _Account {
  const _Account(this.name, this.handle, this.followers, this.reason, this.color);
  final String name;
  final String handle;
  final String followers;
  final String reason;
  final Color color;
}

class _SocialSetupFollowScreenState extends State<SocialSetupFollowScreen> {
  static const List<_Account> _accounts = <_Account>[
    _Account('Maya Chen', 'mayadraws', '128K', 'Popular in Design',
        Color(0xFF9B8CFF)),
    _Account('The Verge', 'verge', '3.2M', 'Popular in Technology',
        Color(0xFF6E56F7)),
    _Account('Jordan Blake', 'jblake', '54K', '3 mutual follows',
        Color(0xFF34D399)),
    _Account('Lens & Light', 'lenslight', '210K', 'Popular in Photography',
        Color(0xFFF4476B)),
    _Account('Priya Nair', 'priyacodes', '88K', 'Suggested for you',
        Color(0xFF38BDF8)),
    _Account('Studio Nord', 'studionord', '17K', 'Popular in Design',
        Color(0xFFFBBF24)),
    _Account('Sam Ortiz', 'samshoots', '41K', '5 mutual follows',
        Color(0xFFEC4899)),
  ];

  late final Set<String> _following = <String>{'mayadraws', 'verge'};

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: SocialSetupFollowScreen._bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _SetupHeader(step: 5, total: 8, onBack: widget.onBack),
              Padding(
                padding: const EdgeInsets.fromLTRB(24, 16, 24, 4),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    const Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text(
                            'Follow a few accounts',
                            style: TextStyle(
                              fontFamily: SocialSetupFollowScreen._font,
                              fontSize: 27,
                              fontWeight: FontWeight.w700,
                              letterSpacing: -0.7,
                              color: SocialSetupFollowScreen._textHi,
                            ),
                          ),
                          SizedBox(height: 8),
                          Text(
                            'Your feed comes alive once you follow people.',
                            style: TextStyle(
                              fontFamily: SocialSetupFollowScreen._font,
                              fontSize: 14.5,
                              height: 1.4,
                              color: SocialSetupFollowScreen._textLo,
                            ),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(width: 12),
                    GestureDetector(
                      onTap: () => setState(() {
                        if (_following.length == _accounts.length) {
                          _following.clear();
                        } else {
                          _following.addAll(
                              _accounts.map((_Account a) => a.handle));
                        }
                      }),
                      child: Padding(
                        padding: const EdgeInsets.only(top: 4),
                        child: Text(
                          _following.length == _accounts.length
                              ? 'Clear'
                              : 'Follow all',
                          style: const TextStyle(
                            fontFamily: SocialSetupFollowScreen._font,
                            fontSize: 14,
                            fontWeight: FontWeight.w600,
                            color: SocialSetupFollowScreen._accent,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: ListView.separated(
                  padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
                  itemCount: _accounts.length,
                  separatorBuilder: (BuildContext context, int i) =>
                      const SizedBox(height: 12),
                  itemBuilder: (BuildContext context, int i) {
                    final _Account a = _accounts[i];
                    final bool following = _following.contains(a.handle);
                    return _AccountRow(
                      account: a,
                      following: following,
                      onToggle: () => setState(() {
                        if (following) {
                          _following.remove(a.handle);
                        } else {
                          _following.add(a.handle);
                        }
                      }),
                    );
                  },
                ),
              ),
              _SetupFooter(
                count: _following.length,
                onContinue: widget.onContinue,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _AccountRow extends StatelessWidget {
  const _AccountRow({
    required this.account,
    required this.following,
    this.onToggle,
  });
  final _Account account;
  final bool following;
  final VoidCallback? onToggle;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: SocialSetupFollowScreen._surface,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: SocialSetupFollowScreen._hairline),
      ),
      child: Row(
        children: <Widget>[
          _Monogram(name: account.name, color: account.color, size: 46),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  children: <Widget>[
                    Flexible(
                      child: Text(
                        account.name,
                        overflow: TextOverflow.ellipsis,
                        style: const TextStyle(
                          fontFamily: SocialSetupFollowScreen._font,
                          fontSize: 15,
                          fontWeight: FontWeight.w600,
                          color: SocialSetupFollowScreen._textHi,
                        ),
                      ),
                    ),
                    const SizedBox(width: 6),
                    Text(
                      '· ${account.followers}',
                      style: const TextStyle(
                        fontFamily: SocialSetupFollowScreen._font,
                        fontSize: 12.5,
                        fontWeight: FontWeight.w500,
                        color: SocialSetupFollowScreen._muted,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 2),
                Text(
                  account.reason,
                  style: const TextStyle(
                    fontFamily: SocialSetupFollowScreen._font,
                    fontSize: 12.5,
                    color: SocialSetupFollowScreen._muted,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(width: 10),
          _FollowButton(following: following, onTap: onToggle),
        ],
      ),
    );
  }
}

class _FollowButton extends StatelessWidget {
  const _FollowButton({required this.following, this.onTap});
  final bool following;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: following
          ? SocialSetupFollowScreen._surfaceAlt
          : SocialSetupFollowScreen._brand,
      borderRadius: BorderRadius.circular(11),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(11),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(11),
            border: Border.all(
              color: following
                  ? SocialSetupFollowScreen._hairline
                  : SocialSetupFollowScreen._brand,
            ),
          ),
          child: Text(
            following ? 'Following' : 'Follow',
            style: TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 13.5,
              fontWeight: FontWeight.w600,
              color: following ? SocialSetupFollowScreen._textLo : Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

class _Monogram extends StatelessWidget {
  const _Monogram({required this.name, required this.color, required this.size});
  final String name;
  final Color color;
  final double size;

  String get _initials {
    final List<String> parts = name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String s) => s.isNotEmpty)
        .toList();
    if (parts.isEmpty) return '?';
    if (parts.length == 1) return parts.first[0].toUpperCase();
    return (parts.first[0] + parts.last[0]).toUpperCase();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: color.withValues(alpha: 0.18),
        border: Border.all(color: color.withValues(alpha: 0.5)),
      ),
      child: Center(
        child: Text(
          _initials,
          style: TextStyle(
            fontFamily: SocialSetupFollowScreen._font,
            fontSize: size * 0.36,
            fontWeight: FontWeight.w700,
            color: color,
          ),
        ),
      ),
    );
  }
}

class _SetupHeader extends StatelessWidget {
  const _SetupHeader({required this.step, required this.total, this.onBack});
  final int step;
  final int total;
  final VoidCallback? onBack;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 8, 24, 4),
      child: Row(
        children: <Widget>[
          IconButton(
            onPressed: onBack,
            icon: const Icon(Icons.arrow_back_ios_new,
                size: 18, color: SocialSetupFollowScreen._textHi),
          ),
          Expanded(
            child: Row(
              children: List<Widget>.generate(total, (int i) {
                return Expanded(
                  child: Container(
                    height: 4,
                    margin: const EdgeInsets.symmetric(horizontal: 3),
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(2),
                      color: i < step
                          ? SocialSetupFollowScreen._brand
                          : SocialSetupFollowScreen._hairline,
                    ),
                  ),
                );
              }),
            ),
          ),
          const SizedBox(width: 12),
          Text(
            '$step/$total',
            style: const TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 12.5,
              fontWeight: FontWeight.w600,
              color: SocialSetupFollowScreen._muted,
            ),
          ),
        ],
      ),
    );
  }
}

class _SetupFooter extends StatelessWidget {
  const _SetupFooter({required this.count, this.onContinue});
  final int count;
  final VoidCallback? onContinue;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: SocialSetupFollowScreen._bg,
        border:
            Border(top: BorderSide(color: SocialSetupFollowScreen._hairline)),
      ),
      padding: const EdgeInsets.fromLTRB(24, 14, 24, 16),
      child: SizedBox(
        width: double.infinity,
        height: 54,
        child: FilledButton(
          onPressed: onContinue,
          style: FilledButton.styleFrom(
            backgroundColor: SocialSetupFollowScreen._brand,
            foregroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(15),
            ),
          ),
          child: Text(
            count == 0 ? 'Skip for now' : 'Continue · Following $count',
            style: const TextStyle(
              fontFamily: SocialSetupFollowScreen._font,
              fontSize: 16,
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      ),
    );
  }
}

Plus bundled 1 binary asset (fonts/images). The CLI and MCP install those for you automatically.

Two faster ways to add it

Copy-paste works, but you can skip it entirely.

1. FlutterKit CLI

One command drops this screen — and its fonts — straight into your project.

$ flutterkit add social-setup-follow

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install social-setup-follow — it fetches and writes the files for you.

FAQ

Can I use this suggested follows screen in a commercial app?

Yes. FlutterKit screens are free for personal and commercial projects under an MIT-style licence. There is no licence key to enter; run `flutterkit add social-setup-follow`, drop `SocialSetupFollowScreen` into your onboarding flow, and ship it.

How do I load the suggested accounts from my API instead of the hard-coded list?

Replace the `static const List<_Account> _accounts` with a field populated from your response, mapping each record to `_Account(name, handle, followers, reason, color)`. Keep `_following` as a `Set<String>` of handles so the per-row toggle, the 'Follow all' / 'Clear' shortcut and the footer count keep working unchanged. When the user taps Continue, `_following` is exactly the set of handles to send to your follow endpoint.

Why does the header button say 'Clear' sometimes?

The label is a ternary on `_following.length == _accounts.length`. While at least one account is unfollowed it reads 'Follow all' and tapping calls `addAll` with every handle; once every account is followed the same comparison is true, the text becomes 'Clear', and tapping calls `_following.clear()`. Because the check reads the Set directly, tapping individual rows also updates the header label automatically.

Do I need any packages, fonts or image assets?

No pub packages: the file imports only `package:flutter/material.dart`. Avatars are `_Monogram` circles computed from initials, so there are no image URLs or asset files. The Inter font is used throughout, and `flutterkit add social-setup-follow` bundles it and registers it in your pubspec for you.

Which Flutter version does this need?

Flutter 3.22 or newer, because the monogram uses `color.withValues(alpha: 0.18)` and the constructors use `super.key`. On an older 3.x SDK, change each `withValues(alpha: x)` to `withOpacity(x)` and the rest of the file compiles as is.

Related screens