Web385 views

How to Build a Crypto Price Alerts List Screen in Flutter (Full Code + Preview)

A trader who sets a price alert and can't find it again just deletes your app. This screen is the management hub for those alerts: an exchange-dark list where Bitcoin, Ethereum, Solana and Chainlink each get a card showing their above-or-below target in green or red, a live toggle to silence the alert without deleting it, and a 'New alert' FAB. You'll build it as a single self-contained Flutter file — typed alert model, tinted ticker badges with no logo assets, and setState-driven switches — ready to wire to any backend through four callbacks.

Aurum · Price Alerts — Web3 Flutter UI screen
Live preview — Aurum · Price Alerts, built in pure Flutter.

What you'll build

  • ✓An exchange-dark card list with tinted circular ticker badges — each coin's brand colour at 16% alpha, no logo images
  • ✓A condition line pairing a north-east or south-east arrow with a green 'Above' or red 'Below' price in tabular figures
  • ✓Per-row Material Switches that flip an alert on or off in place via setState, with an amber active track
  • ✓An extended 'New alert' FAB plus a history action for triggered alerts, kept backend-agnostic through four callbacks
  • ✓A tiny _Alert model whose one mutable field is exactly the one piece of per-row state — the toggle

Step-by-step build

1

Create the file

Add a new file at lib/web3_alerts_list/web3_alerts_list_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.

A stateful list with four exits

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

/// Web3 — Price Alerts. A list of active alert rows (coin, condition, target,
/// on/off toggle) with an add-alert FAB. Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, ticker-badge CustomPaint avoided in favour
/// of tinted circles (no glyphs / no network image), forced dark theme so it
/// renders standalone as a route.
class Web3AlertsListScreen extends StatefulWidget {
  const Web3AlertsListScreen({
    super.key,
    this.onBack,
    this.onCreate,
    this.onViewTriggered,
    this.onAlertTap,
  });

  final VoidCallback? onBack;
  final VoidCallback? onCreate;
  final VoidCallback? onViewTriggered;
  final VoidCallback? onAlertTap;

  @override
  State<Web3AlertsListScreen> createState() => _Web3AlertsListScreenState();
}

`Web3AlertsListScreen` is a `StatefulWidget` for one reason only: the per-row toggles mutate. Everything else that could change the world leaves through four nullable `VoidCallback`s — `onBack`, `onCreate`, `onViewTriggered`, `onAlertTap`. Creating an alert, opening one, and jumping to fired-alert history are all navigation concerns, so the screen fires a callback and lets the host app decide what a 'create alert' route even looks like. The doc comment spells out the constraints the file honours: pure Flutter, bundled Inter, tinted circles instead of a ticker-badge `CustomPaint`, and a forced dark theme so the screen renders correctly as a standalone route.

The exchange palette and four seed alerts

web3_alerts_list_screen.dart
class _Web3AlertsListScreenState extends State<Web3AlertsListScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _surfaceAlt = Color(0xFF1E2329);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _up = Color(0xFF2EBD85);
  static const Color _down = Color(0xFFF6465D);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  final List<_Alert> _alerts = <_Alert>[
    _Alert('BTC', 'Bitcoin', true, '68,000.00', Color(0xFFF0B90B), true),
    _Alert('ETH', 'Ethereum', false, '2,500.00', Color(0xFF627EEA), true),
    _Alert('SOL', 'Solana', true, '180.00', Color(0xFF14F195), false),
    _Alert('LINK', 'Chainlink', false, '12.50', Color(0xFF2A5ADA), false),
  ];

The palette is straight off a trading terminal: near-black `#0B0E11` background, `#161A1E` card surface, amber `#F0B90B` brand, and the classic pair `_up` green `#2EBD85` / `_down` red `#F6465D` that will colour every condition. `_muted` `#848E9C` and `_hairline` `#2B3139` handle secondary text and borders. The seed list holds four positional `_Alert` records — BTC, ETH, SOL, LINK — each carrying its coin's real brand colour as `tint` (Ethereum's `#627EEA`, Solana's `#14F195`, Chainlink's `#2A5ADA`). Notice the combinations: BTC is on+above, ETH on+below, SOL off+above, LINK off+below — the demo data deliberately covers all four visual states. The list is `final` but not `const`, because each row's `on` flag must stay mutable.

Forced dark chrome: AppBar, history action, and the FAB

web3_alerts_list_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        appBar: AppBar(
          backgroundColor: _bg,
          elevation: 0,
          centerTitle: true,
          leading: IconButton(
            onPressed: widget.onBack,
            icon: const Icon(Icons.arrow_back_ios_new_rounded,
                size: 18, color: _text),
          ),
          title: const Text(
            'Price alerts',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 17,
              fontWeight: FontWeight.w700,
              color: _text,
            ),
          ),
          actions: <Widget>[
            IconButton(
              onPressed: widget.onViewTriggered,
              icon: const Icon(Icons.history_rounded, size: 21, color: _muted),
            ),
            const SizedBox(width: 4),
          ],
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: widget.onCreate,
          backgroundColor: _brand,
          icon: const Icon(Icons.add_rounded, color: _bg),
          label: const Text(
            'New alert',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 14.5,
              fontWeight: FontWeight.w700,
              color: _bg,
            ),
          ),
        ),

`build` wraps everything in `Theme(data: ThemeData.dark(useMaterial3: true))`, so the screen stays dark even inside a light-themed host app — without it, the Material `Switch` and `AppBar` defaults would leak the ambient theme into an exchange-dark design. The AppBar is flat (`elevation: 0`) on the same `_bg` as the body, with an 18px back chevron and a centred 'Price alerts' title at 17px `w700`. The one action is `Icons.history_rounded` in `_muted`, wired to `onViewTriggered` — standing alerts and already-fired alerts are different mental lists, so history gets its own screen instead of mixing triggered rows in here. The `FloatingActionButton.extended` reads 'New alert' in amber with dark icon and label, because on `#F0B90B` dark text has far better contrast than white.

The list plumbing and the tinted ticker badge

web3_alerts_list_screen.dart
        body: SafeArea(
          top: false,
          child: ListView.separated(
            padding: const EdgeInsets.fromLTRB(20, 12, 20, 96),
            itemCount: _alerts.length,
            separatorBuilder: (BuildContext context, int i) =>
                const SizedBox(height: 12),
            itemBuilder: (BuildContext context, int i) {
              final _Alert a = _alerts[i];
              final Color condColor = a.above ? _up : _down;
              return GestureDetector(
                onTap: widget.onAlertTap,
                behavior: HitTestBehavior.opaque,
                child: Container(
                  padding: const EdgeInsets.all(14),
                  decoration: BoxDecoration(
                    color: _surface,
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: _hairline),
                  ),
                  child: Row(
                    children: <Widget>[
                      Container(
                        width: 42,
                        height: 42,
                        decoration: BoxDecoration(
                          color: a.tint.withValues(alpha: 0.16),
                          shape: BoxShape.circle,
                        ),
                        alignment: Alignment.center,
                        child: Text(
                          a.ticker,
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 11,
                            fontWeight: FontWeight.w800,
                            color: a.tint,
                          ),
                        ),
                      ),

`SafeArea(top: false)` skips the top inset since the AppBar already owns it, and `ListView.separated` puts a 12px gap between cards with padding `fromLTRB(20, 12, 20, 96)` — the 96px bottom is what stops the last card from hiding behind the FAB. Each row derives `condColor` once (`a.above ? _up : _down`) and reuses it for both the arrow and the price. The card is a `GestureDetector` with `HitTestBehavior.opaque`, so taps on the padding between texts still register, wrapping a `_surface` container with a 16px radius and `_hairline` border. The badge is the file's neatest trick: a 42px circle filled with `a.tint.withValues(alpha: 0.16)` holding the ticker text at 11px `w800` in the full tint — coin identity from typography alone, with no logo assets and no network images to fail.

The condition line: direction, colour, and tabular figures

web3_alerts_list_screen.dart
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              a.name,
                              style: const TextStyle(
                                fontFamily: _font,
                                fontSize: 14.5,
                                fontWeight: FontWeight.w700,
                                color: _text,
                              ),
                            ),
                            const SizedBox(height: 4),
                            Row(
                              children: <Widget>[
                                Icon(
                                  a.above
                                      ? Icons.north_east_rounded
                                      : Icons.south_east_rounded,
                                  size: 14,
                                  color: condColor,
                                ),
                                const SizedBox(width: 4),
                                Text(
                                  '${a.above ? 'Above' : 'Below'} \$${a.target}',
                                  style: TextStyle(
                                    fontFamily: _font,
                                    fontSize: 12.5,
                                    fontWeight: FontWeight.w600,
                                    fontFeatures: const <FontFeature>[
                                      FontFeature.tabularFigures()
                                    ],
                                    color: condColor,
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),

The expanded middle column stacks the coin name at 14.5px `w700` over the condition row. That row is where the data becomes glanceable: a 14px `Icons.north_east_rounded` or `south_east_rounded` picked by `a.above`, then the interpolated string `'${a.above ? 'Above' : 'Below'} \$${a.target}'` at 12.5px `w600` — arrow, word, and price all in `condColor`, so green consistently means an above target and red a below target, matching trading conventions. The price style sets `FontFeature.tabularFigures()`, which forces every digit to the same width; without it, '68,000.00' and '12.50' would jitter out of alignment across rows and the list would look untidy the moment two prices differ in digits.

Live toggles and the one-mutable-field model

web3_alerts_list_screen.dart
                      Switch(
                        value: a.on,
                        onChanged: (bool v) => setState(() => a.on = v),
                        activeThumbColor: _bg,
                        activeTrackColor: _brand,
                        inactiveThumbColor: _muted,
                        inactiveTrackColor: _surfaceAlt,
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _Alert {
  _Alert(this.ticker, this.name, this.on, this.target, this.tint, this.above);

  final String ticker;
  final String name;
  bool on;
  final String target;
  final Color tint;
  final bool above;
}

The trailing `Switch` reads `a.on` and its `onChanged` is a one-liner — `setState(() => a.on = v)` — mutating the row's model directly and rebuilding. It works because `_Alert` is designed for exactly this: five `final` fields (ticker, name, target, tint, above) and a single non-final `bool on`, so the toggle is the only per-row state that can change and nothing else can drift. The switch colours are hand-set rather than themed: `activeThumbColor: _bg` on an amber `_brand` track makes an enabled alert pop, while the inactive pair `_muted` on `_surfaceAlt` lets disabled rows recede into the card. In a real app, this `onChanged` is where your pause-alert API call belongs — flip optimistically, then revert on failure.

Full code

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

import 'package:flutter/material.dart';

/// Web3 — Price Alerts. A list of active alert rows (coin, condition, target,
/// on/off toggle) with an add-alert FAB. Self-contained per CONVENTIONS.md:
/// pure Flutter, bundled Inter font, ticker-badge CustomPaint avoided in favour
/// of tinted circles (no glyphs / no network image), forced dark theme so it
/// renders standalone as a route.
class Web3AlertsListScreen extends StatefulWidget {
  const Web3AlertsListScreen({
    super.key,
    this.onBack,
    this.onCreate,
    this.onViewTriggered,
    this.onAlertTap,
  });

  final VoidCallback? onBack;
  final VoidCallback? onCreate;
  final VoidCallback? onViewTriggered;
  final VoidCallback? onAlertTap;

  @override
  State<Web3AlertsListScreen> createState() => _Web3AlertsListScreenState();
}

class _Web3AlertsListScreenState extends State<Web3AlertsListScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _surfaceAlt = Color(0xFF1E2329);
  static const Color _brand = Color(0xFFF0B90B);
  static const Color _up = Color(0xFF2EBD85);
  static const Color _down = Color(0xFFF6465D);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  final List<_Alert> _alerts = <_Alert>[
    _Alert('BTC', 'Bitcoin', true, '68,000.00', Color(0xFFF0B90B), true),
    _Alert('ETH', 'Ethereum', false, '2,500.00', Color(0xFF627EEA), true),
    _Alert('SOL', 'Solana', true, '180.00', Color(0xFF14F195), false),
    _Alert('LINK', 'Chainlink', false, '12.50', Color(0xFF2A5ADA), false),
  ];

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        appBar: AppBar(
          backgroundColor: _bg,
          elevation: 0,
          centerTitle: true,
          leading: IconButton(
            onPressed: widget.onBack,
            icon: const Icon(Icons.arrow_back_ios_new_rounded,
                size: 18, color: _text),
          ),
          title: const Text(
            'Price alerts',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 17,
              fontWeight: FontWeight.w700,
              color: _text,
            ),
          ),
          actions: <Widget>[
            IconButton(
              onPressed: widget.onViewTriggered,
              icon: const Icon(Icons.history_rounded, size: 21, color: _muted),
            ),
            const SizedBox(width: 4),
          ],
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: widget.onCreate,
          backgroundColor: _brand,
          icon: const Icon(Icons.add_rounded, color: _bg),
          label: const Text(
            'New alert',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 14.5,
              fontWeight: FontWeight.w700,
              color: _bg,
            ),
          ),
        ),
        body: SafeArea(
          top: false,
          child: ListView.separated(
            padding: const EdgeInsets.fromLTRB(20, 12, 20, 96),
            itemCount: _alerts.length,
            separatorBuilder: (BuildContext context, int i) =>
                const SizedBox(height: 12),
            itemBuilder: (BuildContext context, int i) {
              final _Alert a = _alerts[i];
              final Color condColor = a.above ? _up : _down;
              return GestureDetector(
                onTap: widget.onAlertTap,
                behavior: HitTestBehavior.opaque,
                child: Container(
                  padding: const EdgeInsets.all(14),
                  decoration: BoxDecoration(
                    color: _surface,
                    borderRadius: BorderRadius.circular(16),
                    border: Border.all(color: _hairline),
                  ),
                  child: Row(
                    children: <Widget>[
                      Container(
                        width: 42,
                        height: 42,
                        decoration: BoxDecoration(
                          color: a.tint.withValues(alpha: 0.16),
                          shape: BoxShape.circle,
                        ),
                        alignment: Alignment.center,
                        child: Text(
                          a.ticker,
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 11,
                            fontWeight: FontWeight.w800,
                            color: a.tint,
                          ),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              a.name,
                              style: const TextStyle(
                                fontFamily: _font,
                                fontSize: 14.5,
                                fontWeight: FontWeight.w700,
                                color: _text,
                              ),
                            ),
                            const SizedBox(height: 4),
                            Row(
                              children: <Widget>[
                                Icon(
                                  a.above
                                      ? Icons.north_east_rounded
                                      : Icons.south_east_rounded,
                                  size: 14,
                                  color: condColor,
                                ),
                                const SizedBox(width: 4),
                                Text(
                                  '${a.above ? 'Above' : 'Below'} \$${a.target}',
                                  style: TextStyle(
                                    fontFamily: _font,
                                    fontSize: 12.5,
                                    fontWeight: FontWeight.w600,
                                    fontFeatures: const <FontFeature>[
                                      FontFeature.tabularFigures()
                                    ],
                                    color: condColor,
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                      Switch(
                        value: a.on,
                        onChanged: (bool v) => setState(() => a.on = v),
                        activeThumbColor: _bg,
                        activeTrackColor: _brand,
                        inactiveThumbColor: _muted,
                        inactiveTrackColor: _surfaceAlt,
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _Alert {
  _Alert(this.ticker, this.name, this.on, this.target, this.tint, this.above);

  final String ticker;
  final String name;
  bool on;
  final String target;
  final Color tint;
  final bool above;
}

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 web3-alerts-list

2. AI agent (MCP)

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

FAQ

Is this price alerts screen free to use in a commercial app?

Yes — copy it from this page, install it with the CLI command, or pull it through MCP and ship it in a paid product. There is no licence key, no attribution requirement, and no sign-up.

Does it need any packages or icon assets?

No. It is pure Flutter — Material icons for the arrows and history glyph, and tinted-circle ticker badges instead of coin logo images, so there is nothing to download at runtime. The only asset is the bundled Inter font file referenced by `fontFamily: 'Inter'`.

Which Flutter version does this need?

A recent one. The badge tint uses `Color.withValues(alpha: 0.16)`, which needs Flutter 3.22+ (swap to `withOpacity(0.16)` on older SDKs), and the switch uses `activeThumbColor`, the newer name for the deprecated `activeColor` — rename it back if your SDK predates that change. The constructor also uses `super.key`.

How do I make these alerts fire real notifications?

The screen is deliberately backend-agnostic — it renders alert state but watches no prices. Run the comparisons server-side against a price feed and deliver pushes via FCM or APNs, since a phone can't reliably watch markets in the background. The `Switch.onChanged` callback is where you sync a pause/resume to that backend, and `onViewTriggered` is meant to route to a fired-alerts history screen.

How do I load real alerts instead of the four hard-coded ones?

Replace the seeded `_alerts` list with data from your API, keeping the `_Alert` shape: ticker, name, on, target string, tint colour, and the `above` flag. Format the target with thousands separators before it reaches the widget — the row renders the string verbatim, which is also why `tabularFigures` keeps the prices aligned.

Related screens