DEV3 Technologies home

Wallet card with transaction list

Balance card with gradient and quick actions above a transaction list that colours credits and debits.

FlutterCardsflutterwalletbalancetransactions

Code

import 'package:flutter/material.dart';

class WalletTx {
  const WalletTx(this.title, this.cents, this.at); // positive = money in
  final String title;
  final int cents;
  final DateTime at;
}

String formatMoney(int cents, {String symbol = '\$'}) {
  final abs = cents.abs();
  final whole = (abs ~/ 100).toString().replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (_) => ',');
  return '${cents < 0 ? '-' : ''}$symbol$whole.${(abs % 100).toString().padLeft(2, '0')}';
}

class WalletView extends StatelessWidget {
  const WalletView({super.key, required this.balanceCents, required this.transactions, this.onTopUp, this.onSend});
  final int balanceCents;
  final List<WalletTx> transactions;
  final VoidCallback? onTopUp, onSend;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return CustomScrollView(slivers: [
      SliverToBoxAdapter(
        child: Container(
          margin: const EdgeInsets.all(16),
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            gradient: LinearGradient(
              begin: AlignmentDirectional.topStart,
              end: AlignmentDirectional.bottomEnd,
              colors: [scheme.primary, scheme.tertiary],
            ),
          ),
          child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
            Text('Balance', style: TextStyle(color: scheme.onPrimary.withValues(alpha: 0.8))),
            Text(formatMoney(balanceCents),
                style: Theme.of(context).textTheme.displaySmall?.copyWith(color: scheme.onPrimary)),
            const SizedBox(height: 12),
            Row(children: [
              FilledButton.tonalIcon(onPressed: onTopUp, icon: const Icon(Icons.add), label: const Text('Top up')),
              const SizedBox(width: 8),
              FilledButton.tonalIcon(onPressed: onSend, icon: const Icon(Icons.north_east), label: const Text('Send')),
            ]),
          ]),
        ),
      ),
      SliverList.separated(
        itemCount: transactions.length,
        separatorBuilder: (_, _) => const Divider(height: 1),
        itemBuilder: (context, i) {
          final t = transactions[i];
          final credit = t.cents >= 0;
          return ListTile(
            leading: CircleAvatar(child: Icon(credit ? Icons.south_west : Icons.north_east)),
            title: Text(t.title),
            subtitle: Text(MaterialLocalizations.of(context).formatShortDate(t.at)),
            trailing: Text(
              '${credit ? '+' : ''}${formatMoney(t.cents)}',
              style: TextStyle(fontWeight: FontWeight.w600, color: credit ? Colors.green.shade700 : scheme.onSurface),
            ),
          );
        },
      ),
    ]);
  }
}

How it works

What it solves

Wallet screens combine a prominent balance with a history list. The details that matter are exact money formatting, clear in/out colouring and a layout that scrolls as one piece.

How it works

Money is stored as integer cents. formatMoney inserts thousands separators with a regular expression and always pads two decimal places, so no floating point maths is involved. The screen is a CustomScrollView: the gradient card is one box sliver and the history is a SliverList.separated, so the card scrolls away with the list instead of sitting in a fixed header.

Positive amounts get a plus sign and green text, outgoing ones use the normal text colour so they stay calm. Icons and the gradient direction use directional values for right-to-left locales.

Adapting it

Swap formatMoney for NumberFormat.currency from intl when you need locale-specific symbols and separators. Add a sticky month header with SliverPersistentHeader, or load more rows with the paginated list pattern. Hook the buttons to your own top-up and transfer screens.

Pitfalls

  • Always keep currency in minor units on the wire and in state.
  • withValues(alpha:) needs a current Flutter release; use withOpacity on older SDKs.
  • A fixed green may fail contrast in dark mode; derive it from the theme if you support both.

How to use it

Copy the file to lib/wallet/. No packages.

WalletView(
  balanceCents: 125050,
  transactions: [WalletTx('Top up', 5000, DateTime.now())],
  onTopUp: () {},
)

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets