Fintech85 views

How to Build a Fintech Budget Setup Screen in Flutter (Full Code + Preview)

Every budgeting feature starts with the same screen: pick a category, choose a limit, decide whether the app should warn you. This tutorial walks through a Revolut-style budget-setup screen in Flutter where all three inputs drive real state — a 48px dollar label that re-renders as the slider moves in $50 steps, a three-column category grid whose selection ring costs no layout shift, and a save button whose text names the category you picked. It is pure Flutter with a forced dark theme, and every line below comes from the actual file.

Fintech · Budget Setup — Fintech Flutter UI screen
Live preview — Fintech · Budget Setup, built in pure Flutter.

What you'll build

  • ✓A discrete amount slider from $50 to $3,000 whose 59 divisions snap to clean $50 increments
  • ✓A live 48px monthly-limit label that rounds and re-renders on every slider tick
  • ✓A three-column category grid where selection is a 1.5px brand border swapped over a transparent one, so tapping never shifts the layout
  • ✓An 'Alert at 80%' overspend toggle built as a surface tile with a brand-tracked Switch
  • ✓A pill-shaped save button whose label rewrites itself to 'Create Restaurants budget' (or whichever category is active)

Step-by-step build

1

Create the file

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

Palette, category data and three pieces of state

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

/// Budget setup — create a monthly budget (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, no network images, and the screen forces
/// its own dark theme. A category picker, amount slider and alert toggle all
/// drive live state; the large amount label updates with the slider.
class FintechBudgetSetupScreen extends StatefulWidget {
  const FintechBudgetSetupScreen({super.key, this.onBack, this.onSave});

  final VoidCallback? onBack;
  final VoidCallback? onSave;

  @override
  State<FintechBudgetSetupScreen> createState() =>
      _FintechBudgetSetupScreenState();
}

class _FintechBudgetSetupScreenState extends State<FintechBudgetSetupScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _brand = Color(0xFF494FDF);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _amber = Color(0xFFEC7E00);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);

  static const List<_Cat> _cats = <_Cat>[
    _Cat('Overall', Icons.account_balance_wallet_rounded, _brand),
    _Cat('Restaurants', Icons.restaurant_rounded, _amber),
    _Cat('Shopping', Icons.shopping_bag_rounded, _brand),
    _Cat('Transport', Icons.local_taxi_rounded, _red),
    _Cat('Groceries', Icons.local_grocery_store_rounded, _teal),
    _Cat('Travel', Icons.flight_rounded, _teal),
  ];

  int _cat = 1;
  double _amount = 500;
  bool _alert = true;

The screen is a `StatefulWidget` taking two optional callbacks, `onBack` and `onSave`, so the host app owns navigation and persistence. The state class opens with the whole design system as `static const` colours: `_bg` #191C1F, `_surface` #242729, brand indigo `_brand` #494FDF, plus `_teal` #00A87E, `_amber` #EC7E00 and `_red` #E23B4A for category tints, with `_muted` #8D969E for secondary text. The six categories live in a `static const List<_Cat>` where each entry bundles a name, a rounded Material icon and its own tint — Restaurants is amber, Transport is red, Groceries and Travel are teal — so the grid later just reads `_cats[i]` instead of switching on strings. Only three fields ever mutate: `_cat = 1` (Restaurants preselected, a spending category being a likelier budget than 'Overall'), `_amount = 500`, and `_alert = true` so the safety net is opt-out rather than opt-in.

A forced dark theme and the slider that drives everything

fintech_budget_setup_screen.dart
  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _buildAppBar(),
              Expanded(
                child: ListView(
                  physics: const BouncingScrollPhysics(),
                  padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
                  children: <Widget>[
                    _buildAmount(),
                    const SizedBox(height: 8),
                    SliderTheme(
                      data: SliderTheme.of(context).copyWith(
                        activeTrackColor: _brand,
                        inactiveTrackColor: _surface,
                        thumbColor: _brand,
                        overlayColor: _brand.withValues(alpha: 0.18),
                        trackHeight: 4,
                      ),
                      child: Slider(
                        value: _amount,
                        min: 50,
                        max: 3000,
                        divisions: 59,
                        onChanged: (double v) => setState(() => _amount = v),
                      ),
                    ),
                    const SizedBox(height: 16),
                    _label('Category'),
                    _buildCatGrid(),
                    const SizedBox(height: 24),
                    _buildAlertTile(),
                  ],
                ),
              ),
              _buildSave(),
            ],
          ),
        ),
      ),
    );
  }

`build` wraps the `Scaffold` in `Theme(data: ThemeData.dark(useMaterial3: true))`, so the screen renders dark even inside a light host app — the vendored design depends on it. The layout is a `Column` of three bands: app bar, an `Expanded` `ListView` with `BouncingScrollPhysics`, and the save button pinned below the scroll area so it never disappears off-screen. The interesting numbers are on the `Slider`: `min: 50`, `max: 3000`, `divisions: 59` — (3000 − 50) / 50 = 59, so every detent lands on an exact $50 boundary and the big label never shows an awkward $847. `SliderTheme.copyWith` paints the active track and thumb in `_brand`, reuses `_surface` as the inactive track so the rail reads as part of the background system, and sets the press overlay to `_brand.withValues(alpha: 0.18)` on a 4px track. `onChanged` is one line: `setState(() => _amount = v)`.

Balancing a centred title without an AppBar

fintech_budget_setup_screen.dart
  Widget _buildAppBar() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
      child: Row(
        children: <Widget>[
          IconButton(
            onPressed: widget.onBack,
            icon: const Icon(Icons.arrow_back_ios_new_rounded,
                size: 20, color: Colors.white),
          ),
          const Expanded(
            child: Text(
              'New budget',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontFamily: _font,
                fontSize: 18,
                fontWeight: FontWeight.w500,
                letterSpacing: 0.24,
                color: Colors.white,
              ),
            ),
          ),
          const SizedBox(width: 48),
        ],
      ),
    );
  }

The header is a hand-rolled `Row` rather than a Material `AppBar`: a back `IconButton` with `arrow_back_ios_new_rounded` at 20px, then 'New budget' centred inside an `Expanded`. The trailing `const SizedBox(width: 48)` is the piece that matters — a standard `IconButton` occupies 48 logical pixels, so mirroring that width on the right keeps `textAlign: TextAlign.center` genuinely centred instead of drifting toward the empty side. The title is 18px `w500` with the file's signature `letterSpacing: 0.24`, which every text style in this screen repeats for a consistent slightly-open Inter look.

The live amount readout

fintech_budget_setup_screen.dart
  Widget _buildAmount() {
    return Column(
      children: <Widget>[
        const SizedBox(height: 8),
        const Text(
          'Monthly limit',
          style: TextStyle(
            fontFamily: _font,
            fontSize: 13,
            letterSpacing: 0.24,
            color: _muted,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          '\$${_amount.round()}',
          style: const TextStyle(
            fontFamily: _font,
            fontSize: 48,
            fontWeight: FontWeight.w600,
            letterSpacing: 0.24,
            color: Colors.white,
          ),
        ),
      ],
    );
  }

`_buildAmount` is a two-line `Column`: a 13px `_muted` 'Monthly limit' caption, then the number itself — `'\$${_amount.round()}'` at 48px `w600` white. Because `_amount` is a `double`, `.round()` strips the decimal before display, and since the slider only stops on $50 detents the rounded value is always the true value. This label is the payoff of keeping `_amount` in state: dragging the slider calls `setState`, the whole subtree rebuilds, and the giant number tracks the thumb frame by frame without any listener or controller plumbing.

A category grid where selection costs nothing

fintech_budget_setup_screen.dart
  Widget _buildCatGrid() {
    return GridView.count(
      crossAxisCount: 3,
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 1.6,
      children: <Widget>[
        for (int i = 0; i < _cats.length; i++)
          GestureDetector(
            onTap: () => setState(() => _cat = i),
            child: Container(
              decoration: BoxDecoration(
                color: _surface,
                borderRadius: BorderRadius.circular(14),
                border: Border.all(
                  color: _cat == i ? _brand : Colors.transparent,
                  width: 1.5,
                ),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(_cats[i].icon, size: 22, color: _cats[i].tint),
                  const SizedBox(height: 6),
                  Text(
                    _cats[i].name,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 11.5,
                      fontWeight: FontWeight.w500,
                      letterSpacing: 0.24,
                      color: Colors.white,
                    ),
                  ),
                ],
              ),
            ),
          ),
      ],
    );
  }

`GridView.count` lays the six categories in three columns with `shrinkWrap: true` and `NeverScrollableScrollPhysics`, because the grid lives inside the outer `ListView` and must size itself rather than fight for scroll gestures. `childAspectRatio: 1.6` makes each cell a wide, short card that fits an icon over an 11.5px label. Each cell is a `GestureDetector` writing `_cat = i` on tap, and the selected state is expressed purely as `Border.all(color: _cat == i ? _brand : Colors.transparent, width: 1.5)` — the unselected cells carry an invisible border of the same width, so switching selection recolours a ring instead of adding one, and nothing in the grid ever reflows. Icons take their tint from the data (`_cats[i].tint`), which is why Restaurants glows amber while Groceries is teal, on the shared `_surface` card with a 14px corner radius.

The overspend-alert tile

fintech_budget_setup_screen.dart
  Widget _buildAlertTile() {
    return Container(
      padding: const EdgeInsets.fromLTRB(16, 6, 12, 6),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        children: <Widget>[
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Alert at 80%',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 14.5,
                    fontWeight: FontWeight.w500,
                    letterSpacing: 0.24,
                    color: Colors.white,
                  ),
                ),
                SizedBox(height: 2),
                Text(
                  'Notify me before I overspend',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 12.5,
                    letterSpacing: 0.24,
                    color: _muted,
                  ),
                ),
              ],
            ),
          ),
          Switch(
            value: _alert,
            onChanged: (bool v) => setState(() => _alert = v),
            activeThumbColor: Colors.white,
            activeTrackColor: _brand,
          ),
        ],
      ),
    );
  }

The alert row is a `_surface` container with a 16px radius and deliberately asymmetric padding — `fromLTRB(16, 6, 12, 6)` — because the Material `Switch` on the right ships its own generous touch target; trimming the right inset to 12 keeps the switch looking optically flush with the card edge. The left side stacks 'Alert at 80%' at 14.5px `w500` over a 12.5px `_muted` line, 'Notify me before I overspend', which sells the toggle as a benefit instead of a setting. The `Switch` binds to `_alert` with a white `activeThumbColor` on a `_brand` track, so the on-state matches the slider and the save button rather than defaulting to Material's teal-green.

One shared section label

fintech_budget_setup_screen.dart
  Widget _label(String text) {
    return Padding(
      padding: const EdgeInsets.only(left: 4, bottom: 12),
      child: Text(
        text,
        style: const TextStyle(
          fontFamily: _font,
          fontSize: 12.5,
          fontWeight: FontWeight.w500,
          letterSpacing: 0.24,
          color: _muted,
        ),
      ),
    );
  }

`_label` is a tiny helper producing the 12.5px `_muted` 'Category' heading with `EdgeInsets.only(left: 4, bottom: 12)`. The 4px left inset is optical alignment: the grid cards below have visible edges while the label's text box does not, so nudging the text right makes the two appear flush. Keeping it a method means any future section — a period picker, say — gets an identical heading for free.

A save button that names its category

fintech_budget_setup_screen.dart
  Widget _buildSave() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
      child: SizedBox(
        width: double.infinity,
        height: 56,
        child: Material(
          color: _brand,
          borderRadius: BorderRadius.circular(9999),
          child: InkWell(
            borderRadius: BorderRadius.circular(9999),
            onTap: widget.onSave,
            child: Center(
              child: Text(
                'Create ${_cats[_cat].name} budget',
                style: const TextStyle(
                  fontFamily: _font,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _Cat {
  const _Cat(this.name, this.icon, this.tint);
  final String name;
  final IconData icon;
  final Color tint;
}

`_buildSave` skips `FilledButton` in favour of `Material` + `InkWell`, both with a `BorderRadius.circular(9999)` — an effectively infinite radius that turns a full-width 56px `SizedBox` into a perfect pill while keeping the ink ripple clipped to it. Doing it manually avoids fighting `ButtonStyle` for an exact height and radius. The label is the screen's best trick: `'Create ${_cats[_cat].name} budget'` interpolates the selected category, so tapping Travel instantly relabels the button 'Create Travel budget' — confirmation of the choice at the moment of commitment, with no extra state. `onTap` forwards to `widget.onSave`. The file closes with `_Cat`, a three-field const value class (name, icon, tint) that lets the category list at the top stay a compile-time constant.

Full code

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

import 'package:flutter/material.dart';

/// Budget setup — create a monthly budget (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, no network images, and the screen forces
/// its own dark theme. A category picker, amount slider and alert toggle all
/// drive live state; the large amount label updates with the slider.
class FintechBudgetSetupScreen extends StatefulWidget {
  const FintechBudgetSetupScreen({super.key, this.onBack, this.onSave});

  final VoidCallback? onBack;
  final VoidCallback? onSave;

  @override
  State<FintechBudgetSetupScreen> createState() =>
      _FintechBudgetSetupScreenState();
}

class _FintechBudgetSetupScreenState extends State<FintechBudgetSetupScreen> {
  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _brand = Color(0xFF494FDF);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _amber = Color(0xFFEC7E00);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);

  static const List<_Cat> _cats = <_Cat>[
    _Cat('Overall', Icons.account_balance_wallet_rounded, _brand),
    _Cat('Restaurants', Icons.restaurant_rounded, _amber),
    _Cat('Shopping', Icons.shopping_bag_rounded, _brand),
    _Cat('Transport', Icons.local_taxi_rounded, _red),
    _Cat('Groceries', Icons.local_grocery_store_rounded, _teal),
    _Cat('Travel', Icons.flight_rounded, _teal),
  ];

  int _cat = 1;
  double _amount = 500;
  bool _alert = true;

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(useMaterial3: true),
      child: Scaffold(
        backgroundColor: _bg,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              _buildAppBar(),
              Expanded(
                child: ListView(
                  physics: const BouncingScrollPhysics(),
                  padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
                  children: <Widget>[
                    _buildAmount(),
                    const SizedBox(height: 8),
                    SliderTheme(
                      data: SliderTheme.of(context).copyWith(
                        activeTrackColor: _brand,
                        inactiveTrackColor: _surface,
                        thumbColor: _brand,
                        overlayColor: _brand.withValues(alpha: 0.18),
                        trackHeight: 4,
                      ),
                      child: Slider(
                        value: _amount,
                        min: 50,
                        max: 3000,
                        divisions: 59,
                        onChanged: (double v) => setState(() => _amount = v),
                      ),
                    ),
                    const SizedBox(height: 16),
                    _label('Category'),
                    _buildCatGrid(),
                    const SizedBox(height: 24),
                    _buildAlertTile(),
                  ],
                ),
              ),
              _buildSave(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildAppBar() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
      child: Row(
        children: <Widget>[
          IconButton(
            onPressed: widget.onBack,
            icon: const Icon(Icons.arrow_back_ios_new_rounded,
                size: 20, color: Colors.white),
          ),
          const Expanded(
            child: Text(
              'New budget',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontFamily: _font,
                fontSize: 18,
                fontWeight: FontWeight.w500,
                letterSpacing: 0.24,
                color: Colors.white,
              ),
            ),
          ),
          const SizedBox(width: 48),
        ],
      ),
    );
  }

  Widget _buildAmount() {
    return Column(
      children: <Widget>[
        const SizedBox(height: 8),
        const Text(
          'Monthly limit',
          style: TextStyle(
            fontFamily: _font,
            fontSize: 13,
            letterSpacing: 0.24,
            color: _muted,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          '\$${_amount.round()}',
          style: const TextStyle(
            fontFamily: _font,
            fontSize: 48,
            fontWeight: FontWeight.w600,
            letterSpacing: 0.24,
            color: Colors.white,
          ),
        ),
      ],
    );
  }

  Widget _buildCatGrid() {
    return GridView.count(
      crossAxisCount: 3,
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 1.6,
      children: <Widget>[
        for (int i = 0; i < _cats.length; i++)
          GestureDetector(
            onTap: () => setState(() => _cat = i),
            child: Container(
              decoration: BoxDecoration(
                color: _surface,
                borderRadius: BorderRadius.circular(14),
                border: Border.all(
                  color: _cat == i ? _brand : Colors.transparent,
                  width: 1.5,
                ),
              ),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(_cats[i].icon, size: 22, color: _cats[i].tint),
                  const SizedBox(height: 6),
                  Text(
                    _cats[i].name,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 11.5,
                      fontWeight: FontWeight.w500,
                      letterSpacing: 0.24,
                      color: Colors.white,
                    ),
                  ),
                ],
              ),
            ),
          ),
      ],
    );
  }

  Widget _buildAlertTile() {
    return Container(
      padding: const EdgeInsets.fromLTRB(16, 6, 12, 6),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        children: <Widget>[
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Alert at 80%',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 14.5,
                    fontWeight: FontWeight.w500,
                    letterSpacing: 0.24,
                    color: Colors.white,
                  ),
                ),
                SizedBox(height: 2),
                Text(
                  'Notify me before I overspend',
                  style: TextStyle(
                    fontFamily: _font,
                    fontSize: 12.5,
                    letterSpacing: 0.24,
                    color: _muted,
                  ),
                ),
              ],
            ),
          ),
          Switch(
            value: _alert,
            onChanged: (bool v) => setState(() => _alert = v),
            activeThumbColor: Colors.white,
            activeTrackColor: _brand,
          ),
        ],
      ),
    );
  }

  Widget _label(String text) {
    return Padding(
      padding: const EdgeInsets.only(left: 4, bottom: 12),
      child: Text(
        text,
        style: const TextStyle(
          fontFamily: _font,
          fontSize: 12.5,
          fontWeight: FontWeight.w500,
          letterSpacing: 0.24,
          color: _muted,
        ),
      ),
    );
  }

  Widget _buildSave() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
      child: SizedBox(
        width: double.infinity,
        height: 56,
        child: Material(
          color: _brand,
          borderRadius: BorderRadius.circular(9999),
          child: InkWell(
            borderRadius: BorderRadius.circular(9999),
            onTap: widget.onSave,
            child: Center(
              child: Text(
                'Create ${_cats[_cat].name} budget',
                style: const TextStyle(
                  fontFamily: _font,
                  fontSize: 16,
                  fontWeight: FontWeight.w500,
                  letterSpacing: 0.24,
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _Cat {
  const _Cat(this.name, this.icon, this.tint);
  final String name;
  final IconData icon;
  final Color tint;
}

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 fintech-budget-setup

2. AI agent (MCP)

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

FAQ

Is this budget setup screen free for commercial projects?

Yes. FlutterKit screens are free, commercial use included — no licence key, no attribution line, no upgrade tier. Copy the code from this page or install it via the CLI and ship it in a production fintech app as-is.

Does it depend on any packages or fonts?

No third-party packages — the file imports only `package:flutter/material.dart`, and the category icons are built-in Material icons. The typeface is Inter, bundled with the screen under `fonts/` and referenced everywhere through the `_font` constant, so the layout looks identical on iOS and Android.

Which Flutter version do I need?

Flutter 3.22 or newer: the slider overlay uses `_brand.withValues(alpha: 0.18)`, which older SDKs lack. On an older stable, swap it for `withOpacity(0.18)`; the `Switch`'s `activeThumbColor` is also recent, so replace it with `activeColor` if your SDK does not know it. The constructor already uses `super.key`, standard since Dart 2.17.

Why does the slider use divisions: 59?

It is derived, not arbitrary: the range runs from $50 to $3,000, a span of $2,950, and 2,950 ÷ 50 = 59. Each detent is therefore exactly $50, which is why the 48px label can call `.round()` and always show a clean figure. To change the step, keep the formula — divisions = (max − min) / step.

How do I actually save the budget the user configured?

The screen deliberately stops at UI state: `_cats[_cat].name`, `_amount` and `_alert` hold everything the user chose, and the pill button fires `widget.onSave`. In your app, either read those values by lifting them into the callback's signature (change `onSave` to accept a budget object) or hoist the three fields into a controller and write them to your store — local database, Firestore, or your API — when the callback fires.

Related screens