Web333 views

How to Build a Swap From Token Selector in Flutter (Full Code + Preview)

Before a swap can quote anything, the user has to say what they are giving up, and offering a coin with nothing in it is a dead end. This tutorial builds Aurum's Swap from selector in Flutter: a search box scoped to tokens you already hold, a YOUR TOKENS list where every row pairs the coin with its balance and a comma-grouped dollar value in aligned columns, a no-match fallback, and a single callback that returns the chosen ticker to your swap flow.

Aurum · Swap From — Web3 Flutter UI screen
Live preview — Aurum · Swap From, built in pure Flutter.

What you'll build

  • ✓A holdings-only token list of six assets where each row carries both a raw balance and a dollar value
  • ✓A case-insensitive search that narrows the list by ticker or coin name, with a centred no-match message
  • ✓A regex-based `_fiat` formatter that turns 11810.2 into '$11,810.20' without the intl package
  • ✓Right-aligned balance and value columns locked to tabular figures so digits line up row to row
  • ✓An `onSelect(String)` hook that returns only the ticker, ready to feed the From card of a swap screen

Step-by-step build

1

Create the file

Add a new file at lib/web3_swap_select_from/web3_swap_select_from_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 contract and a table of holdings

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

/// Web3 swap — Select from-token. A searchable list of tokens you hold, sorted
/// by balance, with painted ticker badges, balance and fiat value. Self-contained
/// per CONVENTIONS.md: pure Flutter, bundled Inter font, painted ticker badges
/// (no glyphs / no network image), forced dark theme so it renders standalone as
/// a route.
class Web3SwapSelectFromScreen extends StatefulWidget {
  const Web3SwapSelectFromScreen({super.key, this.onBack, this.onSelect});

  final VoidCallback? onBack;
  final ValueChanged<String>? onSelect;

  @override
  State<Web3SwapSelectFromScreen> createState() =>
      _Web3SwapSelectFromScreenState();
}

class _Web3SwapSelectFromScreenState extends State<Web3SwapSelectFromScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  // Only tokens with a balance are swappable as "from".
  static const List<_Tok> _tokens = <_Tok>[
    _Tok('ETH', 'Ethereum', 1.842, 5061.40, Color(0xFF627EEA)),
    _Tok('USDC', 'USD Coin', 1200.0, 1200.00, Color(0xFF2775CA)),
    _Tok('BTC', 'Bitcoin', 0.184213, 11810.20, Color(0xFFF7931A)),
    _Tok('SOL', 'Solana', 24.5, 3746.05, Color(0xFF14F195)),
    _Tok('BNB', 'BNB', 2.1, 1252.23, Color(0xFFF0B90B)),
    _Tok('MATIC', 'Polygon', 540.0, 318.60, Color(0xFF8247E5)),
  ];

`Web3SwapSelectFromScreen` exposes `onBack` and `onSelect`, a `ValueChanged<String>?`, so the parent swap screen gets back nothing but a ticker and decides for itself how to price it. The comment above `_tokens` states the rule this screen exists to enforce: only tokens with a balance can be a swap source. Each `_Tok` therefore stores two numbers, `balance` (1.842 ETH, 0.184213 BTC) and a precomputed `fiat` value (5061.40, 11810.20), plus a brand `tint` such as `#627EEA` for Ethereum or `#14F195` for Solana. Worth knowing before you ship it: the file's header says the list is sorted by balance, but no sort exists anywhere — the const list renders in written order, so ETH at $5,061 sits above BTC at $11,810.

One query, one derived list

web3_swap_select_from_screen.dart
  String _query = '';

  List<_Tok> get _filtered {
    if (_query.isEmpty) return _tokens;
    final String q = _query.toLowerCase();
    return _tokens
        .where((_Tok t) =>
            t.ticker.toLowerCase().contains(q) ||
            t.name.toLowerCase().contains(q))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    final List<_Tok> list = _filtered;
    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(
            'Swap from',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 17,
              fontWeight: FontWeight.w700,
              color: _text,
            ),
          ),
        ),

`_query` is the only mutable field, and `_filtered` derives the visible rows from it on demand: an empty query returns `_tokens` untouched, otherwise both sides are lowercased and matched with `contains` against `ticker` and `name`. That means 'coin' finds USD Coin and 'poly' finds MATIC, but typing '1200' finds nothing, because balances and dollar values are never searched. `build` reads the getter once into a local `list`, so the empty check, `itemCount` and `itemBuilder` all see the same snapshot within a frame. The Scaffold sits inside a forced `ThemeData.dark` wrapper; its `AppBar` centres a 17px `w700` 'Swap from' title with an 18px rounded back chevron bound to `widget.onBack`.

A search box framed as your tokens

web3_swap_select_from_screen.dart
        body: SafeArea(
          top: false,
          child: Column(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 14),
                  decoration: BoxDecoration(
                    color: _surface,
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(color: _hairline),
                  ),
                  child: Row(
                    children: <Widget>[
                      const Icon(Icons.search_rounded, size: 20, color: _muted),
                      const SizedBox(width: 10),
                      Expanded(
                        child: TextField(
                          onChanged: (String v) => setState(() => _query = v),
                          style: const TextStyle(
                            fontFamily: _font,
                            fontSize: 14.5,
                            color: _text,
                          ),
                          cursorColor: const Color(0xFFF0B90B),
                          decoration: const InputDecoration(
                            isCollapsed: true,
                            border: InputBorder.none,
                            contentPadding: EdgeInsets.symmetric(vertical: 14),
                            hintText: 'Search your tokens',
                            hintStyle: TextStyle(
                              fontFamily: _font,
                              fontSize: 14.5,
                              color: _muted,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const Padding(
                padding: EdgeInsets.fromLTRB(20, 8, 20, 4),
                child: Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    'YOUR TOKENS',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 11,
                      fontWeight: FontWeight.w700,
                      letterSpacing: 0.6,
                      color: _muted,
                    ),
                  ),
                ),
              ),

The body is wrapped in `SafeArea(top: false)` because the AppBar already handles the notch; only the bottom inset needs protecting. The search field is a borderless `TextField` (`isCollapsed: true`, `InputBorder.none`) inside a `_surface` Container with a 12px radius and `_hairline` outline, padded with `contentPadding` of 14px vertically so the tappable height comes from the text field, not the box. The hint reads 'Search your tokens' rather than a generic 'Search', which quietly tells users why an asset they don't own is missing. There is no controller, so the field cannot be cleared or prefilled from code. The 11px uppercase 'YOUR TOKENS' caption with 0.6 letter spacing reinforces the same holdings-only boundary.

Empty fallback and the tap-to-select list

web3_swap_select_from_screen.dart
              Expanded(
                child: list.isEmpty
                    ? const Center(
                        child: Text(
                          'No tokens match your search.',
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 13.5,
                            color: _muted,
                          ),
                        ),
                      )
                    : ListView.separated(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 20, vertical: 4),
                        itemCount: list.length,
                        separatorBuilder: (BuildContext context, int i) =>
                            const SizedBox(height: 2),
                        itemBuilder: (BuildContext context, int i) {
                          final _Tok t = list[i];
                          return _TokRow(
                            tok: t,
                            onTap: () => widget.onSelect?.call(t.ticker),
                          );
                        },
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

An `Expanded` gives the list the remaining height below the search and caption. When `list.isEmpty` it swaps in a centred 13.5px muted 'No tokens match your search.' so a typo produces an explanation instead of an empty dark void. Otherwise `ListView.separated` builds `_TokRow`s with 20px side padding and a 2px `SizedBox` separator — the rows carry their own 11px vertical padding, so spacing comes from the rows and the separator is almost cosmetic. Each row's `onTap` is a closure calling `widget.onSelect?.call(t.ticker)`; the null-aware call means the screen still renders and ripples in a preview where no callback has been passed. There is no highlighted current selection — every row looks identical.

The token model and a dependency-free money formatter

web3_swap_select_from_screen.dart
typedef _S = _Web3SwapSelectFromScreenState;

class _Tok {
  const _Tok(this.ticker, this.name, this.balance, this.fiat, this.tint);

  final String ticker;
  final String name;
  final double balance;
  final double fiat;
  final Color tint;
}

class _TokRow extends StatelessWidget {
  const _TokRow({required this.tok, required this.onTap});

  final _Tok tok;
  final VoidCallback onTap;

  String _fiat(double v) {
    final String s = v.toStringAsFixed(2);
    final List<String> parts = s.split('.');
    final String intp = parts[0].replaceAllMapped(
      RegExp(r'\B(?=(\d{3})+(?!\d))'),
      (Match m) => ',',
    );
    return '\$$intp.${parts[1]}';
  }

`typedef _S` aliases the State class so the separate `_TokRow` can read `_S._font`, `_S._text` and `_S._muted` without redeclaring the palette. `_Tok` is a five-field const value class. The interesting part is `_fiat`: it calls `toStringAsFixed(2)` to guarantee exactly two decimals, splits on the dot, then groups the integer part with `RegExp(r'\B(?=(\d{3})+(?!\d))')`. That pattern matches every non-boundary position followed by a multiple of three digits running to the end of the number, and `replaceAllMapped` inserts a comma at each one, so '11810' becomes '11,810'. The result is reassembled as '$11,810.20' with an escaped dollar sign — no `intl` dependency for a single currency display.

Row ripple and the tinted ticker badge

web3_swap_select_from_screen.dart
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: InkWell(
        borderRadius: BorderRadius.circular(14),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 11),
          child: Row(
            children: <Widget>[
              Container(
                width: 40,
                height: 40,
                decoration: BoxDecoration(
                  color: tok.tint.withValues(alpha: 0.16),
                  shape: BoxShape.circle,
                  border: Border.all(color: tok.tint.withValues(alpha: 0.5)),
                ),
                alignment: Alignment.center,
                child: Text(
                  tok.ticker.length <= 3
                      ? tok.ticker
                      : tok.ticker.substring(0, 3),
                  style: TextStyle(
                    fontFamily: _S._font,
                    fontSize: tok.ticker.length >= 4 ? 9 : 11,
                    fontWeight: FontWeight.w700,
                    color: Color.lerp(tok.tint, Colors.white, 0.4),
                  ),
                ),
              ),

Each row is an `InkWell` with a 14px radius on a transparent `Material`, so the press highlight is a rounded patch rather than a full-width rectangle. The 40px badge is a circle filled with `tok.tint.withValues(alpha: 0.16)` and outlined at alpha 0.5, with the ticker drawn in `Color.lerp(tok.tint, Colors.white, 0.4)` so it stays readable on its own dim fill — a Container and a Text, not a CustomPainter. Tickers longer than three characters are cut with `substring(0, 3)` and drop from 11px to 9px. That works for MATIC ('MAT') but has a side effect here: USDC is four characters, so its badge reads 'USD', which is fine beside the full name but ambiguous if you reuse the badge alone.

Name on the left, money on the right

web3_swap_select_from_screen.dart
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      tok.name,
                      style: const TextStyle(
                        fontFamily: _S._font,
                        fontSize: 14.5,
                        fontWeight: FontWeight.w600,
                        color: _S._text,
                      ),
                    ),
                    const SizedBox(height: 3),
                    Text(
                      tok.ticker,
                      style: const TextStyle(
                        fontFamily: _S._font,
                        fontSize: 12,
                        color: _S._muted,
                      ),
                    ),
                  ],
                ),
              ),
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Text(
                    '${tok.balance} ${tok.ticker}',
                    style: const TextStyle(
                      fontFamily: _S._font,
                      fontSize: 14,
                      fontWeight: FontWeight.w600,
                      fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                      color: _S._text,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    _fiat(tok.fiat),
                    style: const TextStyle(
                      fontFamily: _S._font,
                      fontSize: 12.5,
                      fontWeight: FontWeight.w500,
                      fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                      color: _S._muted,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

An `Expanded` Column stacks the 14.5px `w600` coin name over a 12px muted ticker, taking all spare width so long names push against the numbers rather than overflowing. The trailing Column is aligned with `CrossAxisAlignment.end` and holds the balance at 14px `w600` above the formatted dollar value at 12.5px in muted grey; both use `FontFeature.tabularFigures()` so every digit is the same width and amounts line up down the list. One gotcha: the balance is interpolated as `'${tok.balance} ${tok.ticker}'`, which uses Dart's default double `toString`, so USD Coin shows '1200.0 USDC' with a trailing zero and no grouping. Run it through a formatter like `_fiat` before showing real wallet data.

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 swap — Select from-token. A searchable list of tokens you hold, sorted
/// by balance, with painted ticker badges, balance and fiat value. Self-contained
/// per CONVENTIONS.md: pure Flutter, bundled Inter font, painted ticker badges
/// (no glyphs / no network image), forced dark theme so it renders standalone as
/// a route.
class Web3SwapSelectFromScreen extends StatefulWidget {
  const Web3SwapSelectFromScreen({super.key, this.onBack, this.onSelect});

  final VoidCallback? onBack;
  final ValueChanged<String>? onSelect;

  @override
  State<Web3SwapSelectFromScreen> createState() =>
      _Web3SwapSelectFromScreenState();
}

class _Web3SwapSelectFromScreenState extends State<Web3SwapSelectFromScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF0B0E11);
  static const Color _surface = Color(0xFF161A1E);
  static const Color _text = Color(0xFFEAECEF);
  static const Color _muted = Color(0xFF848E9C);
  static const Color _hairline = Color(0xFF2B3139);

  // Only tokens with a balance are swappable as "from".
  static const List<_Tok> _tokens = <_Tok>[
    _Tok('ETH', 'Ethereum', 1.842, 5061.40, Color(0xFF627EEA)),
    _Tok('USDC', 'USD Coin', 1200.0, 1200.00, Color(0xFF2775CA)),
    _Tok('BTC', 'Bitcoin', 0.184213, 11810.20, Color(0xFFF7931A)),
    _Tok('SOL', 'Solana', 24.5, 3746.05, Color(0xFF14F195)),
    _Tok('BNB', 'BNB', 2.1, 1252.23, Color(0xFFF0B90B)),
    _Tok('MATIC', 'Polygon', 540.0, 318.60, Color(0xFF8247E5)),
  ];

  String _query = '';

  List<_Tok> get _filtered {
    if (_query.isEmpty) return _tokens;
    final String q = _query.toLowerCase();
    return _tokens
        .where((_Tok t) =>
            t.ticker.toLowerCase().contains(q) ||
            t.name.toLowerCase().contains(q))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    final List<_Tok> list = _filtered;
    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(
            'Swap from',
            style: TextStyle(
              fontFamily: _font,
              fontSize: 17,
              fontWeight: FontWeight.w700,
              color: _text,
            ),
          ),
        ),
        body: SafeArea(
          top: false,
          child: Column(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 14),
                  decoration: BoxDecoration(
                    color: _surface,
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(color: _hairline),
                  ),
                  child: Row(
                    children: <Widget>[
                      const Icon(Icons.search_rounded, size: 20, color: _muted),
                      const SizedBox(width: 10),
                      Expanded(
                        child: TextField(
                          onChanged: (String v) => setState(() => _query = v),
                          style: const TextStyle(
                            fontFamily: _font,
                            fontSize: 14.5,
                            color: _text,
                          ),
                          cursorColor: const Color(0xFFF0B90B),
                          decoration: const InputDecoration(
                            isCollapsed: true,
                            border: InputBorder.none,
                            contentPadding: EdgeInsets.symmetric(vertical: 14),
                            hintText: 'Search your tokens',
                            hintStyle: TextStyle(
                              fontFamily: _font,
                              fontSize: 14.5,
                              color: _muted,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const Padding(
                padding: EdgeInsets.fromLTRB(20, 8, 20, 4),
                child: Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    'YOUR TOKENS',
                    style: TextStyle(
                      fontFamily: _font,
                      fontSize: 11,
                      fontWeight: FontWeight.w700,
                      letterSpacing: 0.6,
                      color: _muted,
                    ),
                  ),
                ),
              ),
              Expanded(
                child: list.isEmpty
                    ? const Center(
                        child: Text(
                          'No tokens match your search.',
                          style: TextStyle(
                            fontFamily: _font,
                            fontSize: 13.5,
                            color: _muted,
                          ),
                        ),
                      )
                    : ListView.separated(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 20, vertical: 4),
                        itemCount: list.length,
                        separatorBuilder: (BuildContext context, int i) =>
                            const SizedBox(height: 2),
                        itemBuilder: (BuildContext context, int i) {
                          final _Tok t = list[i];
                          return _TokRow(
                            tok: t,
                            onTap: () => widget.onSelect?.call(t.ticker),
                          );
                        },
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

typedef _S = _Web3SwapSelectFromScreenState;

class _Tok {
  const _Tok(this.ticker, this.name, this.balance, this.fiat, this.tint);

  final String ticker;
  final String name;
  final double balance;
  final double fiat;
  final Color tint;
}

class _TokRow extends StatelessWidget {
  const _TokRow({required this.tok, required this.onTap});

  final _Tok tok;
  final VoidCallback onTap;

  String _fiat(double v) {
    final String s = v.toStringAsFixed(2);
    final List<String> parts = s.split('.');
    final String intp = parts[0].replaceAllMapped(
      RegExp(r'\B(?=(\d{3})+(?!\d))'),
      (Match m) => ',',
    );
    return '\$$intp.${parts[1]}';
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: InkWell(
        borderRadius: BorderRadius.circular(14),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 11),
          child: Row(
            children: <Widget>[
              Container(
                width: 40,
                height: 40,
                decoration: BoxDecoration(
                  color: tok.tint.withValues(alpha: 0.16),
                  shape: BoxShape.circle,
                  border: Border.all(color: tok.tint.withValues(alpha: 0.5)),
                ),
                alignment: Alignment.center,
                child: Text(
                  tok.ticker.length <= 3
                      ? tok.ticker
                      : tok.ticker.substring(0, 3),
                  style: TextStyle(
                    fontFamily: _S._font,
                    fontSize: tok.ticker.length >= 4 ? 9 : 11,
                    fontWeight: FontWeight.w700,
                    color: Color.lerp(tok.tint, Colors.white, 0.4),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      tok.name,
                      style: const TextStyle(
                        fontFamily: _S._font,
                        fontSize: 14.5,
                        fontWeight: FontWeight.w600,
                        color: _S._text,
                      ),
                    ),
                    const SizedBox(height: 3),
                    Text(
                      tok.ticker,
                      style: const TextStyle(
                        fontFamily: _S._font,
                        fontSize: 12,
                        color: _S._muted,
                      ),
                    ),
                  ],
                ),
              ),
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Text(
                    '${tok.balance} ${tok.ticker}',
                    style: const TextStyle(
                      fontFamily: _S._font,
                      fontSize: 14,
                      fontWeight: FontWeight.w600,
                      fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                      color: _S._text,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    _fiat(tok.fiat),
                    style: const TextStyle(
                      fontFamily: _S._font,
                      fontSize: 12.5,
                      fontWeight: FontWeight.w500,
                      fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
                      color: _S._muted,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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-swap-select-from

2. AI agent (MCP)

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

FAQ

Can I use this swap token selector in a commercial app?

Yes. Copy the Dart from this page, add it with the CLI command, or pull it through MCP and ship it in a paid wallet or exchange product. No licence key is involved and you don't need to credit anyone.

What packages or fonts does it depend on?

None beyond the Flutter SDK. Badges are tinted Containers, the dollar formatting is a hand-written regex instead of `intl`, and there are no images. The one asset is the bundled Inter font, referenced through `fontFamily: 'Inter'` in the `_font` constant.

What Flutter version is required?

Flutter 3.22 or later, because the badge fill and ring use `Color.withValues(alpha: ...)` and the constructor uses the `super.key` parameter. For an older SDK, change the two calls to `withOpacity(0.16)` and `withOpacity(0.5)`, and rewrite the constructor as `{Key? key, this.onBack, this.onSelect} : super(key: key)`.

The header comment says the list is sorted by balance. Is it?

No — `_tokens` is rendered in the order it is written, and nothing in the file sorts it. To show the largest holding first, load your wallet's balances into `_Tok` objects and sort by dollar value before filtering, e.g. `final sorted = [...tokens]..sort((a, b) => b.fiat.compareTo(a.fiat));`, then point `_filtered` at `sorted`. Sorting by `fiat` rather than `balance` matters, since 540 MATIC is worth far less than 0.18 BTC.

How do I stop users picking the token already chosen on the other side of the swap?

This screen has no selected or disabled state, so you need to add one. Pass the current To ticker in as a parameter and either drop it inside `_filtered` with an extra `t.ticker != excluded` condition, or keep the row but set its `onTap` to null and lower its opacity so users see why it can't be chosen.

Related screens