Fintech79 views

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

Most cashflow charts in Flutter start with a charting package, a controller, and a build-size bump — all to draw twelve rectangles. This tutorial builds a Revolut-style cashflow screen with none of that: a paired money-in vs money-out bar chart made from `Expanded`, `FractionallySizedBox`, and rounded `Container`s, a 'Net this month' summary card with a positive pill, and a monthly breakdown list that computes each month's net on the fly. You end up with one self-contained, dark-themed Dart file you can drop straight into any finance app.

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

What you'll build

  • A paired bar chart comparing income and spend across six months, built from plain widgets with zero chart dependencies
  • A 'Net this month' summary card showing +$1,315.00 in teal beside a tinted 'Positive' status pill
  • A monthly breakdown card that lists months newest-first and derives each net from income minus outgoing at build time
  • A reusable `_bar` helper that scales any value against a $5,000 ceiling with `FractionallySizedBox`
  • A forced dark theme with a six-colour Revolut-inspired palette and the bundled Inter font

Step-by-step build

1

Create the file

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

One list of month records drives the whole screen

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

/// Cashflow — money in vs out over time (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, the paired bar chart is built from plain
/// widgets (no charting package, no network), and the screen forces its own
/// dark theme. A net summary and per-month breakdown complete the picture.
class FintechCashflowScreen extends StatelessWidget {
  const FintechCashflowScreen({super.key, this.onBack});

  final VoidCallback? onBack;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);
  static const Color _hairline = Color(0xFF2E3235);

  static const List<_Month> _months = <_Month>[
    _Month('Jan', 4200, 3850),
    _Month('Feb', 4200, 4010),
    _Month('Mar', 4650, 3720),
    _Month('Apr', 4200, 4380),
    _Month('May', 4830, 3515),
    _Month('Jun', 4830, 3515),
  ];

  static const double _maxVal = 5000;

`FintechCashflowScreen` is a `StatelessWidget` with a single `onBack` callback — nothing on this screen mutates, so there is no state class to maintain. The palette is six `static const Color`s: a near-black `_bg` (0xFF191C1F), a lighter `_surface` for cards, `_teal` for money in, `_red` for money out, plus `_muted` and `_hairline` for secondary text and dividers. The interesting decision is `_months`: six `_Month(label, income, outgoing)` records are the only data on the screen — the chart, the legend maths, and the breakdown list are all projections of this one list. `_maxVal = 5000` is the shared chart ceiling every bar is scaled against, so bar heights stay comparable across months.

Forcing dark mode and laying out the scroll

fintech_cashflow_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>[
                    _buildNet(),
                    const SizedBox(height: 24),
                    _buildChart(),
                    const SizedBox(height: 12),
                    _buildLegend(),
                    const SizedBox(height: 24),
                    _sectionLabel('Monthly breakdown'),
                    const SizedBox(height: 6),
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      decoration: BoxDecoration(
                        color: _surface,
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Column(
                        children: <Widget>[
                          for (int i = _months.length - 1; i >= 0; i--) ...<Widget>[
                            if (i != _months.length - 1)
                              const Divider(height: 1, color: _hairline),
                            _MonthRow(month: _months[i]),
                          ],
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

The build wraps everything in `Theme(data: ThemeData.dark(useMaterial3: true))`, so the screen renders dark even inside a light-themed host app — it carries its own theme rather than inheriting one. Inside a `SafeArea`, the app bar stays fixed while an `Expanded` `ListView` with `BouncingScrollPhysics` scrolls the net card, chart, legend, and breakdown. The breakdown card uses a reversed collection-for — `for (int i = _months.length - 1; i >= 0; i--)` — so June appears first, matching how people read a finance history (most recent month on top), and the `if (i != _months.length - 1)` guard inserts a 1px `_hairline` `Divider` between rows but never above the first one.

A hand-balanced app bar

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

Rather than a Material `AppBar`, the header is a plain `Row`: an `IconButton` firing `onBack`, an `Expanded` centred 'Cashflow' title, and a trailing `SizedBox(width: 48)`. That 48px box is the whole trick — it mirrors the standard `IconButton` footprint on the left, so `textAlign: TextAlign.center` lands the title on the true horizontal centre of the screen instead of drifting right. The title is 18px `w500` Inter with a subtle 0.24 letter-spacing that every text style on this screen repeats.

The net summary card and its status pill

fintech_cashflow_screen.dart
  Widget _buildNet() {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(18),
      ),
      child: Row(
        children: <Widget>[
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const <Widget>[
              Text(
                'Net this month',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 13,
                  letterSpacing: 0.24,
                  color: _muted,
                ),
              ),
              SizedBox(height: 6),
              Text(
                r'+$1,315.00',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 28,
                  fontWeight: FontWeight.w600,
                  letterSpacing: 0.24,
                  color: _teal,
                ),
              ),
            ],
          ),
          const Spacer(),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: _teal.withValues(alpha: 0.16),
              borderRadius: BorderRadius.circular(9999),
            ),
            child: const Text(
              'Positive',
              style: TextStyle(
                fontFamily: _font,
                fontSize: 12.5,
                fontWeight: FontWeight.w600,
                letterSpacing: 0.24,
                color: _teal,
              ),
            ),
          ),
        ],
      ),
    );
  }

`_buildNet` is a `_surface` container with 18px padding and an 18px corner radius holding a two-line column — a 13px `_muted` 'Net this month' label over the 28px `w600` figure — pushed apart from a status pill by a `Spacer`. The amount string is a raw literal (`r'+$1,315.00'`) so the dollar sign is not read as Dart interpolation, and it renders in `_teal` because the month is positive. The pill's background is `_teal.withValues(alpha: 0.16)` with a `borderRadius` of 9999 — a tint of the same hue as its 'Positive' text, so the badge reads as a state of the number rather than a separate element.

A paired bar chart with no charting package

fintech_cashflow_screen.dart
  Widget _buildChart() {
    return SizedBox(
      height: 160,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          for (final _Month m in _months)
            Expanded(
              child: Column(
                children: <Widget>[
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        _bar(m.income / _maxVal, _teal),
                        const SizedBox(width: 3),
                        _bar(m.outgoing / _maxVal, _red),
                      ],
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    m.label,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 11,
                      letterSpacing: 0.24,
                      color: _muted,
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }

  Widget _bar(double frac, Color color) {
    return Expanded(
      child: Align(
        alignment: Alignment.bottomCenter,
        child: FractionallySizedBox(
          heightFactor: frac.clamp(0.02, 1),
          child: Container(
            decoration: BoxDecoration(
              color: color,
              borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
            ),
          ),
        ),
      ),
    );
  }

The chart is a 160px-tall `Row` with `crossAxisAlignment: CrossAxisAlignment.end`, one `Expanded` column per month so six months always share the width evenly. Each column stacks a bottom-aligned pair of bars — `_bar(m.income / _maxVal, _teal)` and `_bar(m.outgoing / _maxVal, _red)` separated by a 3px gap — above the 11px month label. `_bar` does the actual scaling: an `Align(alignment: Alignment.bottomCenter)` holding a `FractionallySizedBox` whose `heightFactor` is `frac.clamp(0.02, 1)`, so bars grow up from the baseline and a zero-value month still shows a 2% sliver instead of vanishing. Only the top corners get a 4px radius, keeping the baseline flat.

Legend dots and a spaced-caps section label

fintech_cashflow_screen.dart
  Widget _buildLegend() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        _legendDot(_teal, 'Money in'),
        const SizedBox(width: 24),
        _legendDot(_red, 'Money out'),
      ],
    );
  }

  Widget _legendDot(Color color, String label) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Container(
          width: 10,
          height: 10,
          decoration: BoxDecoration(
            color: color,
            borderRadius: BorderRadius.circular(3),
          ),
        ),
        const SizedBox(width: 8),
        Text(
          label,
          style: const TextStyle(
            fontFamily: _font,
            fontSize: 12.5,
            letterSpacing: 0.24,
            color: _muted,
          ),
        ),
      ],
    );
  }

  Widget _sectionLabel(String text) {
    return Text(
      text.toUpperCase(),
      style: const TextStyle(
        fontFamily: _font,
        fontSize: 11,
        fontWeight: FontWeight.w500,
        letterSpacing: 1.0,
        color: _muted,
      ),
    );
  }

`_buildLegend` centres two `_legendDot` rows 24px apart, each a 10px square with a 3px corner radius beside 12.5px `_muted` text — 'Money in' in `_teal`, 'Money out' in `_red`, the same hues the bars use, so the mapping needs no explanation. `mainAxisSize: MainAxisSize.min` keeps each dot-label pair hugging its content so the outer `mainAxisAlignment: MainAxisAlignment.center` can do the positioning. `_sectionLabel` upper-cases its text at 11px with `letterSpacing: 1.0`, the classic spaced-caps treatment that separates 'MONTHLY BREAKDOWN' from the data below without stealing visual weight.

The month model and a net computed at build time

fintech_cashflow_screen.dart
class _Month {
  const _Month(this.label, this.income, this.outgoing);
  final String label;
  final double income;
  final double outgoing;
}

class _MonthRow extends StatelessWidget {
  const _MonthRow({required this.month});

  final _Month month;

  @override
  Widget build(BuildContext context) {
    final double net = month.income - month.outgoing;
    final bool positive = net >= 0;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 14),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 44,
            child: Text(
              month.label,
              style: const TextStyle(
                fontFamily: FintechCashflowScreen._font,
                fontSize: 14.5,
                fontWeight: FontWeight.w500,
                letterSpacing: 0.24,
                color: Colors.white,
              ),
            ),
          ),
          const Spacer(),
          Text(
            '+\$${month.income.toStringAsFixed(0)}',
            style: const TextStyle(
              fontFamily: FintechCashflowScreen._font,
              fontSize: 13,
              letterSpacing: 0.24,
              color: FintechCashflowScreen._teal,
            ),
          ),
          const SizedBox(width: 14),
          Text(
            '-\$${month.outgoing.toStringAsFixed(0)}',
            style: const TextStyle(
              fontFamily: FintechCashflowScreen._font,
              fontSize: 13,
              letterSpacing: 0.24,
              color: FintechCashflowScreen._red,
            ),
          ),
          const SizedBox(width: 14),
          SizedBox(
            width: 64,
            child: Text(
              '${positive ? '+' : '-'}\$${net.abs().toStringAsFixed(0)}',
              textAlign: TextAlign.right,
              style: TextStyle(
                fontFamily: FintechCashflowScreen._font,
                fontSize: 13.5,
                fontWeight: FontWeight.w600,
                letterSpacing: 0.24,
                color: positive ? Colors.white : FintechCashflowScreen._red,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

`_Month` is a three-field const value class — `label`, `income`, `outgoing` — deliberately storing no net, because `_MonthRow` derives it in build: `final double net = month.income - month.outgoing;` and `final bool positive = net >= 0;`. That keeps the data honest (a stored net could drift out of sync with its parts). The row lays out a fixed 44px month label, a `Spacer`, then income in `_teal` with a `+` prefix, outgoing in `_red` with a `-`, and the net right-aligned in a fixed 64px `SizedBox` so amounts line up down the column. The sign logic is `'${positive ? '+' : '-'}\$${net.abs().toStringAsFixed(0)}'` — the value is shown as an absolute with an explicit sign, and only a negative net turns the text `_red`; a positive net stays white so the column doesn't shout.

Full code

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

import 'package:flutter/material.dart';

/// Cashflow — money in vs out over time (Revolut-inspired design system).
///
/// Self-contained per CONVENTIONS.md: pure Flutter only, the exact design font
/// (Inter) is bundled under `fonts/`, the paired bar chart is built from plain
/// widgets (no charting package, no network), and the screen forces its own
/// dark theme. A net summary and per-month breakdown complete the picture.
class FintechCashflowScreen extends StatelessWidget {
  const FintechCashflowScreen({super.key, this.onBack});

  final VoidCallback? onBack;

  static const String _font = 'Inter';
  static const Color _bg = Color(0xFF191C1F);
  static const Color _surface = Color(0xFF242729);
  static const Color _teal = Color(0xFF00A87E);
  static const Color _red = Color(0xFFE23B4A);
  static const Color _muted = Color(0xFF8D969E);
  static const Color _hairline = Color(0xFF2E3235);

  static const List<_Month> _months = <_Month>[
    _Month('Jan', 4200, 3850),
    _Month('Feb', 4200, 4010),
    _Month('Mar', 4650, 3720),
    _Month('Apr', 4200, 4380),
    _Month('May', 4830, 3515),
    _Month('Jun', 4830, 3515),
  ];

  static const double _maxVal = 5000;

  @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>[
                    _buildNet(),
                    const SizedBox(height: 24),
                    _buildChart(),
                    const SizedBox(height: 12),
                    _buildLegend(),
                    const SizedBox(height: 24),
                    _sectionLabel('Monthly breakdown'),
                    const SizedBox(height: 6),
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      decoration: BoxDecoration(
                        color: _surface,
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Column(
                        children: <Widget>[
                          for (int i = _months.length - 1; i >= 0; i--) ...<Widget>[
                            if (i != _months.length - 1)
                              const Divider(height: 1, color: _hairline),
                            _MonthRow(month: _months[i]),
                          ],
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

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

  Widget _buildNet() {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(18),
      ),
      child: Row(
        children: <Widget>[
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const <Widget>[
              Text(
                'Net this month',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 13,
                  letterSpacing: 0.24,
                  color: _muted,
                ),
              ),
              SizedBox(height: 6),
              Text(
                r'+$1,315.00',
                style: TextStyle(
                  fontFamily: _font,
                  fontSize: 28,
                  fontWeight: FontWeight.w600,
                  letterSpacing: 0.24,
                  color: _teal,
                ),
              ),
            ],
          ),
          const Spacer(),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: _teal.withValues(alpha: 0.16),
              borderRadius: BorderRadius.circular(9999),
            ),
            child: const Text(
              'Positive',
              style: TextStyle(
                fontFamily: _font,
                fontSize: 12.5,
                fontWeight: FontWeight.w600,
                letterSpacing: 0.24,
                color: _teal,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildChart() {
    return SizedBox(
      height: 160,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          for (final _Month m in _months)
            Expanded(
              child: Column(
                children: <Widget>[
                  Expanded(
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        _bar(m.income / _maxVal, _teal),
                        const SizedBox(width: 3),
                        _bar(m.outgoing / _maxVal, _red),
                      ],
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    m.label,
                    style: const TextStyle(
                      fontFamily: _font,
                      fontSize: 11,
                      letterSpacing: 0.24,
                      color: _muted,
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }

  Widget _bar(double frac, Color color) {
    return Expanded(
      child: Align(
        alignment: Alignment.bottomCenter,
        child: FractionallySizedBox(
          heightFactor: frac.clamp(0.02, 1),
          child: Container(
            decoration: BoxDecoration(
              color: color,
              borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildLegend() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        _legendDot(_teal, 'Money in'),
        const SizedBox(width: 24),
        _legendDot(_red, 'Money out'),
      ],
    );
  }

  Widget _legendDot(Color color, String label) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Container(
          width: 10,
          height: 10,
          decoration: BoxDecoration(
            color: color,
            borderRadius: BorderRadius.circular(3),
          ),
        ),
        const SizedBox(width: 8),
        Text(
          label,
          style: const TextStyle(
            fontFamily: _font,
            fontSize: 12.5,
            letterSpacing: 0.24,
            color: _muted,
          ),
        ),
      ],
    );
  }

  Widget _sectionLabel(String text) {
    return Text(
      text.toUpperCase(),
      style: const TextStyle(
        fontFamily: _font,
        fontSize: 11,
        fontWeight: FontWeight.w500,
        letterSpacing: 1.0,
        color: _muted,
      ),
    );
  }
}

class _Month {
  const _Month(this.label, this.income, this.outgoing);
  final String label;
  final double income;
  final double outgoing;
}

class _MonthRow extends StatelessWidget {
  const _MonthRow({required this.month});

  final _Month month;

  @override
  Widget build(BuildContext context) {
    final double net = month.income - month.outgoing;
    final bool positive = net >= 0;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 14),
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 44,
            child: Text(
              month.label,
              style: const TextStyle(
                fontFamily: FintechCashflowScreen._font,
                fontSize: 14.5,
                fontWeight: FontWeight.w500,
                letterSpacing: 0.24,
                color: Colors.white,
              ),
            ),
          ),
          const Spacer(),
          Text(
            '+\$${month.income.toStringAsFixed(0)}',
            style: const TextStyle(
              fontFamily: FintechCashflowScreen._font,
              fontSize: 13,
              letterSpacing: 0.24,
              color: FintechCashflowScreen._teal,
            ),
          ),
          const SizedBox(width: 14),
          Text(
            '-\$${month.outgoing.toStringAsFixed(0)}',
            style: const TextStyle(
              fontFamily: FintechCashflowScreen._font,
              fontSize: 13,
              letterSpacing: 0.24,
              color: FintechCashflowScreen._red,
            ),
          ),
          const SizedBox(width: 14),
          SizedBox(
            width: 64,
            child: Text(
              '${positive ? '+' : '-'}\$${net.abs().toStringAsFixed(0)}',
              textAlign: TextAlign.right,
              style: TextStyle(
                fontFamily: FintechCashflowScreen._font,
                fontSize: 13.5,
                fontWeight: FontWeight.w600,
                letterSpacing: 0.24,
                color: positive ? Colors.white : FintechCashflowScreen._red,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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-cashflow

2. AI agent (MCP)

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

FAQ

Can I use this cashflow screen in a commercial Flutter app?

Yes. FlutterKit screens are free to use, including commercially. You can ship this cashflow screen in a client project, a paid fintech app, or an internal tool without attribution.

Which packages does this screen need?

None. The paired bar chart is built entirely from `Row`, `Expanded`, `Align`, and `FractionallySizedBox` — no fl_chart or other charting dependency, and no network calls. The only asset is the Inter font, which ships bundled with the screen under `fonts/` and is referenced by the `_font` constant.

What Flutter version does this code require?

Flutter 3.27 or newer, because the status pill uses `Color.withValues(alpha: 0.16)`, which replaced the deprecated `withOpacity`. On an older SDK, swap that one call to `_teal.withOpacity(0.16)`. The `super.key` constructor parameter also assumes Dart 2.17+, which any recent Flutter includes.

How do I feed the chart real transaction data?

Replace the `static const _months` list with a list you build from your backend — one `_Month(label, income, outgoing)` per period — and pass it into the widget as a constructor parameter instead of a static. Also compute `_maxVal` from the data (for example `months.map((m) => max(m.income, m.outgoing)).reduce(max)`) so the tallest bar always reaches full height; with the hard-coded 5000 ceiling, a big month would clamp and a quiet year would look flat.

Why does the monthly breakdown list run newest-first while the chart runs oldest-first?

They serve different reading modes. The chart's `for (final _Month m in _months)` keeps chronological order because a trend only makes sense left-to-right in time. The breakdown card iterates backwards (`for (int i = _months.length - 1; i >= 0; i--)`) so June sits at the top — when you scan a list of months you want the most recent one without scrolling, the same convention banking apps use for statements.

Related screens