Finance23 views

How to Build a Send Money Screen in Flutter (Full Code + Preview)

Every P2P payments app needs a hub that answers 'send money how, and to whom?' on one screen. This tutorial builds that hub from the Finco kit: a top bar, a 'Send Money' title, a four-way option selector (Bank, TopUp, QR Code, Nearby) driven by a single selected index, a recent-receipts avatar strip, and a white 'Add New Contact' card holding a search field and two invite rows in different states. You'll learn the data-driven Row pattern the selector uses, and how the card gets its own contained styling.

Wallet · Send Money — Finance Flutter UI screen
Live preview — Wallet · Send Money, built in pure Flutter.

What you'll build

  • A four-way option selector generated from a const List<List<String>> rather than four copy-pasted widgets
  • A collection-for loop with spread syntax that inserts gaps between items but not after the last one
  • A single _option int of state that drives which card is highlighted
  • An 'Add New Contact' card with a shadowed white surface, a search field and Invited / Invite rows
  • A custom search field with the Material underline stripped out and a trailing search icon

Step-by-step build

1

Create the file

Add a new file at lib/wallet_send_money/wallet_send_money_screen.dart in your Flutter project.

2

Register the bundled fonts

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

pubspec.yaml
flutter:
  fonts:
    - family: Aileron
      fonts:
        - asset: fonts/Aileron-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 options table and the screen's single piece of state

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

import 'widgets/palette.dart';
import 'widgets/sections.dart';

/// "Send Money" screen from the Wallet App UI Kit (Finco): a top bar, a header,
/// a four-way option selector, a recent-receipts strip and an "Add New Contact"
/// card with a search field and invite rows.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron) and
/// the kit's icons + avatars. Fully responsive — content scrolls and width caps
/// on large screens. Renders standalone when pushed as a route.
class WalletSendMoneyScreen extends StatefulWidget {
  const WalletSendMoneyScreen({super.key});

  @override
  State<WalletSendMoneyScreen> createState() => _WalletSendMoneyScreenState();
}

class _WalletSendMoneyScreenState extends State<WalletSendMoneyScreen> {
  int _option = 1; // TopUp selected by default.

  static const List<List<String>> _options = <List<String>>[
    <String>['icon_home.png', 'Bank'],
    <String>['topup_icon.png', 'TopUp'],
    <String>['qrcode_icon.png', 'QR Code'],
    <String>['nearby_icon.png', 'Nearby'],
  ];

Two local imports — the shared P palette and a sections file holding OptionCard, RecentReceipts and InviteRow — and no pub packages. The screen's entire state is one int, _option, initialised to 1 so TopUp is selected on first paint. Above it sits _options, a static const List<List<String>> where each inner list is [icon file, label]. Storing the four options as data rather than as four hand-written widgets is what keeps the build method short and makes adding a fifth option a one-line change.

The width-capped scrolling body

wallet_send_money_screen.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: P.bg,
      body: SafeArea(
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 480),
            child: SingleChildScrollView(
              physics: const BouncingScrollPhysics(),
              padding: const EdgeInsets.fromLTRB(22, 24, 22, 32),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  const _TopBar(),
                  const SizedBox(height: 18),
                  const Text('Send Money', style: P.title),
                  const SizedBox(height: 25),
                  const Text('Select Option', style: P.sectionHeader),
                  const SizedBox(height: 20),

Scaffold takes P.bg from the palette, SafeArea clears the notch, and Center + ConstrainedBox(maxWidth: 480) stops the layout stretching across a tablet — on a phone that constraint does nothing, which is exactly the point. The SingleChildScrollView uses BouncingScrollPhysics for the iOS-style rubber-band feel and pads 22px left/right, 24px top, 32px bottom. CrossAxisAlignment.start left-aligns the header and section labels, both of which pull their styles (P.title, P.sectionHeader) from the palette so typography stays consistent with the rest of the kit.

Building the four-way selector

wallet_send_money_screen.dart
                  Row(
                    children: <Widget>[
                      for (int i = 0; i < _options.length; i++) ...<Widget>[
                        Expanded(
                          child: OptionCard(
                            asset: _options[i][0],
                            label: _options[i][1],
                            active: _option == i,
                            onTap: () => setState(() => _option = i),
                          ),
                        ),
                        if (i != _options.length - 1)
                          const SizedBox(width: 8),
                      ],
                    ],
                  ),
                  const SizedBox(height: 31),
                  const RecentReceipts(),
                  const SizedBox(height: 14),
                  const _AddContactCard(),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

This Row is the most interesting code on the screen. A collection-for loop walks the _options list, and the ...<Widget>[] spread lets each iteration contribute more than one child: an Expanded OptionCard plus, conditionally, an 8px SizedBox. The if (i != _options.length - 1) guard is what prevents a trailing gap after 'Nearby', which would push the row's spacing off-centre. Each card is told whether it's active via _option == i and gets an onTap that calls setState — so the four cards share one integer instead of four booleans that could drift out of sync. Below, RecentReceipts and _AddContactCard finish the page.

The Add New Contact card

wallet_send_money_screen.dart
class _AddContactCard extends StatelessWidget {
  const _AddContactCard();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        boxShadow: P.cardShadow,
      ),
      padding: const EdgeInsets.fromLTRB(24, 23, 24, 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const <Widget>[
          Text('Add New Contact', style: P.sectionHeader),
          SizedBox(height: 14),
          _SearchField(),
          SizedBox(height: 6),
          InviteRow(
            asset: 'sendme_imgone.png',
            name: 'Sir James',
            phone: '+0 000 0000',
            invited: true,
          ),
          InviteRow(
            asset: 'sendme_imgtwo.png',
            name: 'Sir James',
            phone: '+0 000 0000',
            invited: false,
          ),
        ],
      ),
    );
  }
}

_AddContactCard is a full-width white Container with a 14px radius and P.cardShadow — the shared shadow every card-like surface in the kit uses, so they're all lit the same way. Its 24/23/24/24 padding is asymmetric by one pixel at the top, matching the original design rather than rounding it off. Inside, a section header, the search field, then two InviteRows that differ only by invited: true versus false — passing the state in as a flag means one widget renders both the 'Invited' and 'Invite' variants instead of two near-identical widgets.

The custom search field

wallet_send_money_screen.dart
class _SearchField extends StatelessWidget {
  const _SearchField();

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: P.searchFill,
        borderRadius: BorderRadius.circular(5),
        border: Border.all(color: P.cyan, width: 0.5),
      ),
      child: const Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              cursorColor: P.blue,
              style: TextStyle(
                fontFamily: P.font,
                fontSize: 14,
                color: P.name,
              ),
              decoration: InputDecoration(
                hintText: 'Search contacts…',
                hintStyle: TextStyle(
                  fontFamily: P.font,
                  fontSize: 14,
                  color: Color(0xFF9AA3AD),
                ),
                isDense: true,
                border: InputBorder.none,
                contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(right: 16),
            child: Icon(Icons.search, size: 20, color: P.ink),
          ),
        ],
      ),
    );
  }
}

The field is a Container styled by hand — P.searchFill background, a small 5px radius, and a hairline 0.5px P.cyan border — with the TextField inside stripped of Material decoration: border: InputBorder.none, isDense: true, and explicit contentPadding of 16px horizontal / 14px vertical. That's the standard recipe when you want the visible box to be your own Container rather than Flutter's InputDecorator. The search icon is a sibling in the Row rather than a suffixIcon, which gives you exact control over its 16px right padding instead of inheriting InputDecoration's built-in spacing.

Full code

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

import 'package:flutter/material.dart';

import 'widgets/palette.dart';
import 'widgets/sections.dart';

/// "Send Money" screen from the Wallet App UI Kit (Finco): a top bar, a header,
/// a four-way option selector, a recent-receipts strip and an "Add New Contact"
/// card with a search field and invite rows.
///
/// Self-contained, pure Flutter. Bundles its exact design font (Aileron) and
/// the kit's icons + avatars. Fully responsive — content scrolls and width caps
/// on large screens. Renders standalone when pushed as a route.
class WalletSendMoneyScreen extends StatefulWidget {
  const WalletSendMoneyScreen({super.key});

  @override
  State<WalletSendMoneyScreen> createState() => _WalletSendMoneyScreenState();
}

class _WalletSendMoneyScreenState extends State<WalletSendMoneyScreen> {
  int _option = 1; // TopUp selected by default.

  static const List<List<String>> _options = <List<String>>[
    <String>['icon_home.png', 'Bank'],
    <String>['topup_icon.png', 'TopUp'],
    <String>['qrcode_icon.png', 'QR Code'],
    <String>['nearby_icon.png', 'Nearby'],
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: P.bg,
      body: SafeArea(
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 480),
            child: SingleChildScrollView(
              physics: const BouncingScrollPhysics(),
              padding: const EdgeInsets.fromLTRB(22, 24, 22, 32),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  const _TopBar(),
                  const SizedBox(height: 18),
                  const Text('Send Money', style: P.title),
                  const SizedBox(height: 25),
                  const Text('Select Option', style: P.sectionHeader),
                  const SizedBox(height: 20),
                  Row(
                    children: <Widget>[
                      for (int i = 0; i < _options.length; i++) ...<Widget>[
                        Expanded(
                          child: OptionCard(
                            asset: _options[i][0],
                            label: _options[i][1],
                            active: _option == i,
                            onTap: () => setState(() => _option = i),
                          ),
                        ),
                        if (i != _options.length - 1)
                          const SizedBox(width: 8),
                      ],
                    ],
                  ),
                  const SizedBox(height: 31),
                  const RecentReceipts(),
                  const SizedBox(height: 14),
                  const _AddContactCard(),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _TopBar extends StatelessWidget {
  const _TopBar();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 40,
      child: Row(
        children: <Widget>[
          Image.asset('${P.img}/icon_menu.png',
              height: 18, filterQuality: FilterQuality.high),
          const Spacer(),
          Image.asset('${P.img}/icon_dashboard.png',
              height: 24, filterQuality: FilterQuality.high),
        ],
      ),
    );
  }
}

class _AddContactCard extends StatelessWidget {
  const _AddContactCard();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        boxShadow: P.cardShadow,
      ),
      padding: const EdgeInsets.fromLTRB(24, 23, 24, 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const <Widget>[
          Text('Add New Contact', style: P.sectionHeader),
          SizedBox(height: 14),
          _SearchField(),
          SizedBox(height: 6),
          InviteRow(
            asset: 'sendme_imgone.png',
            name: 'Sir James',
            phone: '+0 000 0000',
            invited: true,
          ),
          InviteRow(
            asset: 'sendme_imgtwo.png',
            name: 'Sir James',
            phone: '+0 000 0000',
            invited: false,
          ),
        ],
      ),
    );
  }
}

class _SearchField extends StatelessWidget {
  const _SearchField();

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: P.searchFill,
        borderRadius: BorderRadius.circular(5),
        border: Border.all(color: P.cyan, width: 0.5),
      ),
      child: const Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              cursorColor: P.blue,
              style: TextStyle(
                fontFamily: P.font,
                fontSize: 14,
                color: P.name,
              ),
              decoration: InputDecoration(
                hintText: 'Search contacts…',
                hintStyle: TextStyle(
                  fontFamily: P.font,
                  fontSize: 14,
                  color: Color(0xFF9AA3AD),
                ),
                isDense: true,
                border: InputBorder.none,
                contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(right: 16),
            child: Icon(Icons.search, size: 20, color: P.ink),
          ),
        ],
      ),
    );
  }
}

Plus bundled 13 binary assets (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 wallet-send-money

2. AI agent (MCP)

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

FAQ

Is this Flutter send-money screen free to use?

Yes. The full Dart source on this page — the screen plus its palette and section widgets — is free for personal and commercial projects. Install it with the FlutterKit CLI (flutterkit add wallet-send-money) or have an AI agent add it via MCP.

How do I make the search field filter contacts?

Give the TextField a controller or an onChanged callback, lift the query into _WalletSendMoneyScreenState, and build the InviteRows from a filtered list instead of the two hard-coded demo rows. Nothing about the card's layout has to change.

Does it need any external packages?

No — it's pure Flutter on the material library. It bundles the Aileron font plus the kit's option icons and contact avatars, registered in pubspec.yaml as shown in step 2; the CLI and MCP install those files for you.

Which Flutter version does it target?

The supporting widgets use Color.withValues(), so it targets Flutter 3.22+ (Dart 3). It also uses const Row with children, which needs Dart 3's const-collection improvements. On an older SDK, swap withValues(alpha: x) for withOpacity(x).

Related screens