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; usewithOpacityon 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.