Empty States31 views

How to Build an Empty Wallet State in Flutter (Full Code + Preview)

An empty state is the screen a user sees before they've done anything, and a good one nudges them toward the next action instead of feeling broken. This tutorial builds the 'Payment Methods' empty state for a wallet: a centered heading, a friendly cat-with-cards illustration that scales to the screen, a two-line explainer, and a circular blue '+' button pinned near the bottom to add the first card. You'll learn how flex Spacers center the content on tall phones while a scroll fallback prevents overflow on short ones — all in pure Flutter with one bundled font.

Empty Wallet — Empty States Flutter UI screen
Live preview — Empty Wallet, built in pure Flutter.

What you'll build

  • A clean white 'Payment Methods' empty-state screen with a centered 21px heading
  • A responsive illustration that scales to 58% of screen width and keeps its 726:844 aspect ratio
  • A two-line supporting message that explains why the screen is empty and what to do next
  • A circular brand-blue '+' button with a soft shadow and a press-scale animation
  • A layout that centers on tall devices and scrolls instead of overflowing on short ones

Step-by-step build

1

Create the file

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

Imports, the screen class, and design tokens

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

/// "Empty Wallet" empty-state screen from the Empty States UI Kit: a centred
/// title ("Payment Methods"), a friendly cat-with-cards illustration, a short
/// supporting line, and a circular "+" action button pinned near the bottom to
/// add the first payment method.
///
/// Self-contained, pure Flutter. Bundles its exact design font (SF Compact
/// Text) and the kit's original illustration (converted to WebP). Fully
/// responsive — flex spacers centre the content on tall devices and a scroll
/// fallback prevents overflow on short ones. Renders standalone when pushed as
/// a route.
class EmptyWalletScreen extends StatelessWidget {
  const EmptyWalletScreen({super.key});

  static const Color _ink = Color(0xFF1B3554);
  static const Color _body = Color(0xFF344B67);
  static const Color _blue = Color(0xFF4476FF);
  static const String _font = 'Inter';
  static const String _image =
      'lib/screens/empty_states/empty_wallet/images/empty_wallet_cat.webp';

The file imports only Flutter's material library, then declares EmptyWalletScreen as a StatelessWidget — nothing on this screen changes, so stateless is correct. Four constants act as design tokens: _ink (#1B3554) is the dark navy for the heading, _body (#344B67) is the softer slate for the body copy, _blue (#4476FF) is the brand blue for the add button, and _font holds 'Inter'. A fifth constant, _image, stores the asset path to the WebP cat illustration. Naming these once keeps the palette and asset in a single place.

A responsive, scroll-safe scaffold

empty_wallet_screen.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            // Illustration scales to the viewport width but stays within a
            // sensible range; height follows the artwork's 726:844 ratio.
            final double imgWidth =
                (constraints.maxWidth * 0.58).clamp(190.0, 300.0);
            final double imgHeight = imgWidth * (844 / 726);

            return SingleChildScrollView(
              physics: const ClampingScrollPhysics(),
              child: ConstrainedBox(
                constraints: BoxConstraints(minHeight: constraints.maxHeight),
                child: IntrinsicHeight(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 24),

build() returns a white Scaffold wrapped in SafeArea to dodge the notch and home indicator. LayoutBuilder hands you the live constraints so the illustration can size itself: imgWidth is 58% of the available width, clamped between 190 and 300 logical pixels, and imgHeight multiplies that by 844/726 to preserve the artwork's aspect ratio. Then the classic scroll-safe trio — SingleChildScrollView with ClampingScrollPhysics, a ConstrainedBox(minHeight: maxHeight), and IntrinsicHeight — lets the content fill a tall screen but scroll rather than overflow on a short one. A symmetric 24px horizontal Padding insets everything from the edges.

Heading and the scaling illustration

empty_wallet_screen.dart
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        const Spacer(flex: 2),
                        const Text(
                          'Payment Methods',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontFamily: _font,
                            fontWeight: FontWeight.w500,
                            fontSize: 21,
                            height: 1.2,
                            color: _ink,
                          ),
                        ),
                        const Spacer(flex: 1),
                        Image.asset(
                          _image,
                          width: imgWidth,
                          height: imgHeight,
                          fit: BoxFit.contain,
                          filterQuality: FilterQuality.high,
                        ),
                        const Spacer(flex: 1),

A center-aligned Column holds the content, and the vertical rhythm comes entirely from Spacer widgets with different flex weights: Spacer(flex: 2) drops from the top, then Spacer(flex: 1) gaps below the title and again below the image. The 'Payment Methods' Text is 21px, weight w500, line-height 1.2, in the _ink navy. Below it, Image.asset renders the cat illustration at the computed imgWidth/imgHeight with BoxFit.contain and FilterQuality.high so the WebP stays crisp when scaled. Because the sizes are proportional, the composition stays balanced across phone sizes.

The explainer message and the button slot

empty_wallet_screen.dart
                        const Text(
                          "You haven't added any payment methods yet.\n"
                          'Add a card to start sending and receiving money.',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontFamily: _font,
                            fontWeight: FontWeight.w500,
                            fontSize: 16,
                            height: 1.37,
                            color: _body,
                          ),
                        ),
                        const Spacer(flex: 3),
                        const _AddButton(),
                        const SizedBox(height: 34),
                      ],
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

The supporting Text is a single string split across two lines with a \n escape — "You haven't added any payment methods yet." then "Add a card to start sending and receiving money." — rendered center-aligned at 16px, weight w500, line-height 1.37, in the _body slate. A weightier Spacer(flex: 3) pushes the action toward the bottom, where the _AddButton sits above a fixed SizedBox(height: 34) that keeps it clear of the screen's bottom edge. The remaining lines simply close the Column, Padding, IntrinsicHeight, scaffold, and build method.

A tactile circular '+' add button

empty_wallet_screen.dart
/// Circular "+" button with a pressed-scale animation and a soft brand-blue
/// shadow, matching the kit's floating add action.
class _AddButton extends StatefulWidget {
  const _AddButton();

  @override
  State<_AddButton> createState() => _AddButtonState();
}

class _AddButtonState extends State<_AddButton> {
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _pressed = true),
      onTapUp: (_) => setState(() => _pressed = false),
      onTapCancel: () => setState(() => _pressed = false),
      onTap: () {},
      child: AnimatedScale(
        scale: _pressed ? 0.94 : 1,
        duration: const Duration(milliseconds: 150),
        child: Container(
          width: 64,
          height: 64,
          decoration: BoxDecoration(
            color: EmptyWalletScreen._blue,
            shape: BoxShape.circle,
            boxShadow: <BoxShadow>[
              BoxShadow(
                color: EmptyWalletScreen._blue.withValues(alpha: 0.35),
                blurRadius: 18,
                offset: const Offset(0, 8),
              ),
            ],
          ),
          child: const Icon(Icons.add, color: Colors.white, size: 28),
        ),
      ),
    );
  }
}

_AddButton is its own StatefulWidget so it can react to touch. A single _pressed boolean is flipped in onTapDown, onTapUp, and onTapCancel, and AnimatedScale reads it to shrink the button to 0.94 for 150ms while held — that tiny squeeze is what makes it feel pressable. The button is a 64x64 Container with BoxShape.circle filled with the _blue token, plus a soft BoxShadow using _blue.withValues(alpha: 0.35), an 18px blur, and an Offset(0, 8) so the glow sits below it. A white Icons.add at size 28 is centered inside. The real onTap is left empty ({}) for you to wire to your add-card flow.

Full code

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

import 'package:flutter/material.dart';

/// "Empty Wallet" empty-state screen from the Empty States UI Kit: a centred
/// title ("Payment Methods"), a friendly cat-with-cards illustration, a short
/// supporting line, and a circular "+" action button pinned near the bottom to
/// add the first payment method.
///
/// Self-contained, pure Flutter. Bundles its exact design font (SF Compact
/// Text) and the kit's original illustration (converted to WebP). Fully
/// responsive — flex spacers centre the content on tall devices and a scroll
/// fallback prevents overflow on short ones. Renders standalone when pushed as
/// a route.
class EmptyWalletScreen extends StatelessWidget {
  const EmptyWalletScreen({super.key});

  static const Color _ink = Color(0xFF1B3554);
  static const Color _body = Color(0xFF344B67);
  static const Color _blue = Color(0xFF4476FF);
  static const String _font = 'Inter';
  static const String _image =
      'lib/screens/empty_states/empty_wallet/images/empty_wallet_cat.webp';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            // Illustration scales to the viewport width but stays within a
            // sensible range; height follows the artwork's 726:844 ratio.
            final double imgWidth =
                (constraints.maxWidth * 0.58).clamp(190.0, 300.0);
            final double imgHeight = imgWidth * (844 / 726);

            return SingleChildScrollView(
              physics: const ClampingScrollPhysics(),
              child: ConstrainedBox(
                constraints: BoxConstraints(minHeight: constraints.maxHeight),
                child: IntrinsicHeight(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 24),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        const Spacer(flex: 2),
                        const Text(
                          'Payment Methods',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontFamily: _font,
                            fontWeight: FontWeight.w500,
                            fontSize: 21,
                            height: 1.2,
                            color: _ink,
                          ),
                        ),
                        const Spacer(flex: 1),
                        Image.asset(
                          _image,
                          width: imgWidth,
                          height: imgHeight,
                          fit: BoxFit.contain,
                          filterQuality: FilterQuality.high,
                        ),
                        const Spacer(flex: 1),
                        const Text(
                          "You haven't added any payment methods yet.\n"
                          'Add a card to start sending and receiving money.',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontFamily: _font,
                            fontWeight: FontWeight.w500,
                            fontSize: 16,
                            height: 1.37,
                            color: _body,
                          ),
                        ),
                        const Spacer(flex: 3),
                        const _AddButton(),
                        const SizedBox(height: 34),
                      ],
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

/// Circular "+" button with a pressed-scale animation and a soft brand-blue
/// shadow, matching the kit's floating add action.
class _AddButton extends StatefulWidget {
  const _AddButton();

  @override
  State<_AddButton> createState() => _AddButtonState();
}

class _AddButtonState extends State<_AddButton> {
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _pressed = true),
      onTapUp: (_) => setState(() => _pressed = false),
      onTapCancel: () => setState(() => _pressed = false),
      onTap: () {},
      child: AnimatedScale(
        scale: _pressed ? 0.94 : 1,
        duration: const Duration(milliseconds: 150),
        child: Container(
          width: 64,
          height: 64,
          decoration: BoxDecoration(
            color: EmptyWalletScreen._blue,
            shape: BoxShape.circle,
            boxShadow: <BoxShadow>[
              BoxShadow(
                color: EmptyWalletScreen._blue.withValues(alpha: 0.35),
                blurRadius: 18,
                offset: const Offset(0, 8),
              ),
            ],
          ),
          child: const Icon(Icons.add, color: Colors.white, size: 28),
        ),
      ),
    );
  }
}

Plus bundled 2 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 empty-wallet

2. AI agent (MCP)

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

FAQ

Is this empty-state screen free to use?

Yes. The full Dart on this page is free to copy into your own apps, personal or commercial. Paste it directly, run 'flutterkit add empty-wallet' with the CLI, or have an AI agent install it for you via MCP.

Does it need any external packages?

No — it's pure Flutter, built entirely on the material library. It uses one bundled font (Inter) plus the cat illustration asset (empty_wallet_cat.webp), which you register in pubspec.yaml as shown in step 2. The CLI and MCP install the font and image for you automatically.

Which Flutter version does it target?

It uses modern Flutter APIs like Color.withValues() and super parameters, so it targets Flutter 3.22+ (Dart 3). On an older SDK, swap withValues(alpha: 0.35) for withOpacity(0.35) and it will compile.

Related screens