On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Releasing Version 1.0.0
- 2. The Full Session
- 3. Layer 1 — bin/ and the CLI
- 4. Layer 2 — BudgetApp Wires Everything
- 5. Layer 3 — Services Enforce the Rules
- 6. Layer 4 — Repositories and the Files on Disk
- 7. Layer 5 — The Models
- 8. The Concept Map
- 9. The Quality Checks
- 10. Compiling with dart compile exe
- 11. An Honest Review
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 140 — Final Project Review
| Category | 23 — Final Dart Project |
| Lecture | 140 of 140 |
| Estimated video length | 25 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
The Student Budget Tracker is clean and tested. In this last lecture we release it as version 1.0.0, walk through it from
bin/down to the models, map every part to the lecture that taught it, compile it into a real program, and look at where to go after the course.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output in this lecture is real. All sessions were run on Dart 3.12.2 on 2026-10-17. The
democommand puts its data in the current month, so your dates will differ, but the amounts, budgets and alerts will be the same.
Prerequisites
Students should already understand, from Lectures 132–139:
- the project's plan, models, services and repositories (132–136)
- the event stream, the budget monitor and the activity log (137)
- the 53 tests and the refactored CLI in
lib/src/cli/(138–139)
And from earlier: semantic versioning and executables (104), dart compile exe (113), and
documentation (125).
Connection With Previous Lectures
At the end of Lecture 139 the project passed every check: dart analyze found no issues and
dart test ran 53 tests. But it still called itself version 0.1.0, its README said
almost nothing, and the only way to start it was dart run inside the project folder.
Nobody else could use it yet.
A finished program needs a release (version, README, CHANGELOG), a review of every part, and a build that runs without the Dart tools. That is this lecture.
Learning Objectives
By the end of this lecture you will be able to:
- Release a Dart project as version 1.0.0 with a README, a CHANGELOG and
executables: - Run a complete session of the finished app and read each output
- Trace one command through every layer:
bin → cli → services → repository → models - Name the course lecture behind each part of the project
- Compile the project with
dart compile exeand run the result - Judge the project honestly: what is fast enough, what could be improved
- Choose a next step after the course: Flutter, Effective Dart and pub.dev
1. Releasing Version 1.0.0
This lecture changes no logic. Edit these five files in your project:
pubspec.yaml — the version and a command name:
name: budget_tracker
description: Student Budget Tracker - a command-line app for wallets, spending and budgets.
version: 1.0.0
publish_to: none
environment:
sdk: ^3.12.0
executables:
budget_tracker:version: 1.0.0 follows semantic versioning (Lecture 104): before 1.0.0 anything may
change; from now on, a breaking change needs 2.0.0. executables: with an empty value says
"the command budget_tracker runs bin/budget_tracker.dart" — used when the tool is
installed as a global command with dart pub global activate.
lib/src/app_info.dart — the version the help text prints:
/// The current version of the app.
const appVersion = '1.0.0';.gitignore — each user's money data is not source code:
# The app's own data folder
data/CHANGELOG.md — newest version first (Lecture 125):
## 1.0.0
- Command-line interface parsed into a sealed `Command` type (`lib/src/cli/`).
- Exit codes: 0 success, 1 failed command, 64 wrong command line.
- Stricter analysis: strict casts, inference and raw types plus extra lints.
- Tests for models, services, repositories, events and the CLI.
Below it, the ## 0.1.0 entry lists what Lectures 132–137 built.
README.md gets four sections: Running it, Commands (a table), Layout (one line
per folder) and Checks. It should answer "what is this and how do I start it?" in one
screen.
2. The Full Session
Start from an empty data folder (rm -rf data) and run every command once:
$ dart run budget_tracker
Student Budget Tracker v1.0.0
Usage: dart run budget_tracker <command> [arguments]
Commands:
wallets List every wallet with its balance
add-wallet <id> <owner> <name> [opening] Open a new wallet
income <wallet> <amount> <source> Record money coming in
spend <wallet> <amount> <category> [note] Record an expense
transfer <from> <to> <amount> Move money between wallets
budget <category> <limit> Set a monthly limit for a category
report [yyyy-mm] [wallet] Monthly report with budget levels
history <wallet> Show the transactions of one wallet
demo Fill an empty data folder with sample data
$ dart run budget_tracker demo
Demo data created for Ali, Sara and Zayan.
$ dart run budget_tracker add-wallet bilal Bilal "Piggy bank" 250
Opened wallet "bilal" for Bilal.
$ dart run budget_tracker income sara 1500 Scholarship
Recorded t0007: 2026-10-17 Income from Scholarship Rs 1500.00
$ dart run budget_tracker spend ali 200 food pizza with friends
Recorded t0008: 2026-10-17 Food expense (pizza with friends) Rs 200.00
! Budget alert: Food is at 93% of Rs 1500.00 (Near limit)
$ dart run budget_tracker spend zayan 100 transport
Recorded t0009: 2026-10-17 Transport expense Rs 100.00
! Budget alert: Transport is at 140% of Rs 500.00 (OVER BUDGET)
$ dart run budget_tracker transfer sara bilal 300
Recorded t0010: 2026-10-17 Transfer sara -> bilal Rs 300.00
$ dart run budget_tracker budget books 1000
Budget set: Budget(Books: Rs 1000.00)
$ dart run budget_tracker wallets
ali Ali Pocket money Rs 3100.00
sara Sara Bank card Rs 2400.00
zayan Zayan Cash Rs 1300.00
bilal Bilal Piggy bank Rs 550.00
$ dart run budget_tracker report
Report for 2026-10 (all wallets)
Income: Rs 8000.00
Spent: Rs 2900.00
Net: Rs 5100.00
Food Rs 1400.00 / Rs 1500.00 Near limit
Transport Rs 700.00 / Rs 500.00 OVER BUDGET
Books Rs 800.00 / Rs 1000.00 Near limit
Fun Rs 0.00 / Rs 1000.00 OK
Biggest expenses:
2026-10-03 Food expense (groceries) Rs 1200.00
2026-10-04 Books expense Rs 800.00
2026-10-06 Transport expense Rs 600.00
$ dart run budget_tracker history sara
t0004 2026-10-04 Books expense Rs 800.00
t0007 2026-10-17 Income from Scholarship Rs 1500.00
t0010 2026-10-17 Transfer sara -> bilal Rs 300.00
$ dart run budget_tracker spend zayan 5000 fun; echo "exit code: $?"
Error: Wallet "zayan" has only Rs 1300.00, cannot take Rs 5000.00.
exit code: 1
$ dart run budget_tracker spend zayan; echo "exit code: $?"
Error: Usage: spend <wallet> <amount> <category> [note]
exit code: 64All ten user stories from Lecture 132 appear here. Note that the Rs 300 transfer to Bilal is neither income nor spending, and that errors end with exit codes 1 and 64.
3. Layer 1 — bin/ and the CLI
bin/budget_tracker.dart is seven lines:
import 'dart:io';
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}Follow spend ali 200 food down. runCli (lib/src/cli/run_cli.dart) parses, opens the
app, listens for alerts, runs the command, and turns a BudgetException into an exit code:
final app = await BudgetApp.open(dataDir, clock: clock);
final alertsDone = app.monitor
.watch()
.map(formatAlert)
.forEach(out.writeln);
try {
await CliRunner(app, out, clock: clock).run(command);
} finally {
await app.close();
await alertsDone;
}
return exitOk;
} on BudgetException catch (e) {
err.writeln('Error: $e');
return e is UsageException ? exitUsage : exitFailure;
}Command.parse (lib/src/cli/command.dart) matched ['spend', final wallet, final amount, final category, ...final note] — a list pattern (Lecture 079) — and built a SpendCommand.
CliRunner.run (lib/src/cli/cli_runner.dart) destructures it in an exhaustive switch over
the sealed Command (Lecture 082):
case SpendCommand(
:final walletId,
:final amount,
:final category,
:final note,
):
_recorded(
await ledger.recordExpense(walletId, amount, category, note: note),
);4. Layer 2 — BudgetApp Wires Everything
lib/src/budget_app.dart is composition (Lecture 066): it builds three repositories,
loads them in parallel, and hands them to the services:
await Future.wait([wallets.load(), transactions.load(), budgetRepo.load()]);
final ledger = LedgerService(wallets, transactions, clock: clock);
final budgets = BudgetService(budgetRepo, ledger);
final log = ActivityLog.start(file('activity.log'), ledger.events);
return BudgetApp._(ledger, budgets, log: log);The constructor also creates the BudgetMonitor. Both the monitor and the activity log
listen to the same broadcast stream, ledger.events (Lecture 094). close() ends that
stream and waits for every listener — the "await listeners before exit" rule from Lecture 100.
5. Layer 3 — Services Enforce the Rules
LedgerService.recordExpense (lib/src/services/ledger_service.dart) checks the amount,
checks the funds, then calls one generic helper (Lectures 073–074):
/// Saves [transaction], announces it, and hands it back with its own type.
Future<T> _record<T extends Transaction>(T transaction) async {
await _transactions.save(transaction);
_events.add(TransactionRecorded(transaction));
return transaction;
}_events.add wakes the monitor: BudgetMonitor.watch(), an async* function (Lecture
096), matches TransactionRecorded(transaction: Expense(...)) and yields a BudgetAlert
record when the level is .warning or .over.
When the rule fails, _checkFunds throws InsufficientFundsException — one of the six
final subclasses of the sealed BudgetException in lib/src/errors.dart (Lectures
085–086). That is the exit code: 1 line in the session.
6. Layer 4 — Repositories and the Files on Disk
Services never touch files. They depend on one interface (lib/src/repository/repository.dart):
abstract interface class Repository<T extends Entity<Id>, Id> {JsonFileRepository<T, Id> implements it with jsonDecode, JsonEncoder.withIndent and a
FromJson<T> typedef; InMemoryRepository implements it for tests. After the session,
data/transactions.json starts like this — money as whole paisa, dates as ISO strings:
[
{
"type": "income",
"id": "t0001",
"walletId": "ali",
"amount": 500000,
"date": "2026-10-01T00:00:00.000",
"source": "Allowance"
},A broken file becomes a StorageException, never a stack trace (Lecture 139 fixed that).
7. Layer 5 — The Models
The bottom layer depends on nothing but itself. Two pieces carry most of the design.
Money (lib/src/models/money.dart) is an extension type (Lecture 070) over int cents:
extension type const Money(int cents) {and the sealed Transaction (lib/src/models/transaction.dart) answers "how does this
change a wallet?" with an exhaustive switch:
return switch (this) {
Income() => amount,
Expense() => -amount,
Transfer(:final toWalletId) => toWalletId == wallet ? amount : -amount,
};8. The Concept Map
Every folder, and the lectures it uses:
| File(s) | Concepts | Lectures |
|---|---|---|
models/wallet.dart, budget.dart | final class, const and factory constructors, copyWith, ==/hashCode | 053–056, 124, 130 |
models/transaction.dart | sealed class, super parameters, map patterns in fromJson | 059, 061, 080, 082 |
models/money.dart | extension type with operators | 070 |
models/category.dart, monthly_report.dart | enhanced enums, CategoryLine record typedef | 068, 071, 076 |
models/entity.dart, repository/repository.dart | abstract interface class, generic bounds | 062–063, 072–074 |
repository/json_file_repository.dart | files, JSON, FormatException → StorageException | 106–109, 086 |
extensions/ | extension methods, generic groupBy<K> | 069, 073 |
services/ledger_service.dart | async/await, Future.wait, broadcast controller, null-aware ?? (throw ...) | 014, 088–091, 093–094 |
services/budget_monitor.dart | async*, object patterns, dot shorthands | 080, 096, 129 |
errors.dart, events/ | sealed hierarchies, custom exceptions | 082, 085 |
budget_app.dart | composition, private constructor | 057, 066 |
cli/ | list patterns, record destructuring, StringSink, exit codes | 078–079, 081, 113 |
lib/budget_tracker.dart, pubspec.yaml | library exports, packages, semantic versions | 101–105 |
test/, analysis_options.yaml | unit and async tests, lints, strict modes | 114, 117–122 |
9. The Quality Checks
Run the three checks from the README one last time:
$ dart format --output=none --set-exit-if-changed .
Formatted 38 files (0 changed) in 0.04 seconds.
$ dart analyze
Analyzing budget_tracker...
No issues found!
$ dart test
00:00 +53: All tests passed!About 1,500 lines of lib/ and bin/, 53 tests, zero issues.
10. Compiling with dart compile exe
dart run compiles the code every time it starts. For users, build a native program once
(Lecture 113):
$ mkdir -p build && dart compile exe bin/budget_tracker.dart -o build/budget_tracker
Generated: /path/to/budget_tracker/build/budget_tracker
$ ./build/budget_tracker report 2026-10 ali
Report for 2026-10 (ali)
Income: Rs 5000.00
Spent: Rs 1400.00
Net: Rs 3600.00
Food Rs 1400.00 / Rs 1500.00 Near limit
Transport Rs 0.00 / Rs 500.00 OK
Books Rs 0.00 / Rs 1000.00 OK
Fun Rs 0.00 / Rs 1000.00 OK
Biggest expenses:
2026-10-03 Food expense (groceries) Rs 1200.00
2026-10-17 Food expense (pizza with friends) Rs 200.00
$ tail -4 data/activity.log
recorded t0007: 2026-10-17 Income from Scholarship Rs 1500.00
recorded t0008: 2026-10-17 Food expense (pizza with friends) Rs 200.00
recorded t0009: 2026-10-17 Transport expense Rs 100.00
recorded t0010: 2026-10-17 Transfer sara -> bilal Rs 300.00The binary (about 6 MB on macOS) needs no Dart SDK but runs only on the system it was built
for. It found the same data/ because data is a relative path: "the folder I was
started from".
11. An Honest Review
A code review (Lecture 127) also names weaknesses. Three worth knowing:
- Performance (Lecture 126).
balances()callsbalanceOfper wallet, and each call adds up all transactions: wallets × transactions steps. Fine for a student; with millions of rows, a running-balance cache would be the next step. - Arguments. Hand-written list patterns are fine for nine commands. Flags such as
--data <dir>would call forpackage:argsfrom pub.dev. - Features. Recurring income, CSV export, deleting wallets,
dart docAPI pages, andIsolate.run(Lectures 098–099) for heavy statistics.
The best design decision is the one you cannot see in the session: bin/ has no logic.
Everything lives in lib/, behind a public API, tested without a terminal. A different
front end — a web server, or a Flutter app — could reuse lib/ unchanged.
Code Examples
These programs use the project as a library. Create an example/ folder in the project,
save each file there, and run it with dart run example/<file>.dart.
Example 1 — Parsing a Command
import 'package:budget_tracker/budget_tracker.dart';
void main() {
final command = Command.parse(['spend', 'ali', '200', 'food', 'pizza']);
switch (command) {
case SpendCommand(:final walletId, :final amount, :final category):
print('$walletId spends ${amount.format()} on ${category.label}');
default:
print('not a spend');
}
}ali spends Rs 200.00 on FoodExample 2 — Money Arithmetic
import 'package:budget_tracker/budget_tracker.dart';
void main() {
final price = Money.tryParse('12.50')!;
final total = price + price + const Money(5);
print(total.format());
print(total.cents);
print((-total).format());
print(Money.tryParse('twelve'));
}Rs 25.05
2505
-Rs 25.05
nullExample 3 — A Sealed Transaction
import 'package:budget_tracker/budget_tracker.dart';
void main() {
final transfer = Transfer(
id: 't0001',
walletId: 'ali',
amount: const Money(50000),
date: DateTime(2026, 10, 5),
toWalletId: 'zayan',
);
print(transfer.describe());
for (final wallet in ['ali', 'zayan', 'sara']) {
print('$wallet: ${transfer.effectOn(wallet).format()}');
}
}2026-10-05 Transfer ali -> zayan Rs 500.00
ali: -Rs 500.00
zayan: Rs 500.00
sara: Rs 0.00Example 4 — A Generic Repository
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final Repository<Wallet, String> wallets = InMemoryRepository();
await wallets.save(const Wallet(id: 'ali', owner: 'Ali', name: 'Cash'));
await wallets.save(const Wallet(id: 'sara', owner: 'Sara', name: 'Card'));
print(await wallets.count());
print(await wallets.exists('zayan'));
print(await wallets.findById('sara'));
}2
false
Wallet(sara, Sara, Card)count and exists come from the RepositoryQueries extension.
Example 5 — A Service Rule
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final app = BudgetApp.inMemory();
final ledger = app.ledger;
await ledger.openWallet(id: 'ali', owner: 'Ali', name: 'Cash');
await ledger.recordIncome('ali', const Money(30000), source: 'Allowance');
try {
await ledger.recordExpense('ali', const Money(50000), .fun);
} on BudgetException catch (e) {
print('${e.runtimeType}: $e');
}
print((await ledger.balanceOf('ali')).format());
}InsufficientFundsException: Wallet "ali" has only Rs 300.00, cannot take Rs 500.00.
Rs 300.00Example 6 — Listening to Events
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final app = BudgetApp.inMemory();
final ledger = app.ledger;
final done = ledger.events.forEach((event) {
final text = switch (event) {
WalletOpened(:final wallet) => 'opened ${wallet.id}',
TransactionRecorded(:final transaction) => 'saved ${transaction.id}',
};
print(text);
});
await ledger.openWallet(id: 'sara', owner: 'Sara', name: 'Card');
await ledger.recordIncome('sara', const Money(100000), source: 'Scholarship');
await app.close();
await done;
}opened sara
saved t0001Example 7 — Budget Alerts
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final app = BudgetApp.inMemory();
final alerts = app.monitor.watch().map(formatAlert).forEach(print);
await app.budgets.setBudget(.food, const Money(100000));
await app.ledger.openWallet(id: 'ali', owner: 'Ali', name: 'Cash');
await app.ledger.recordIncome(
'ali',
const Money(300000),
source: 'Allowance',
);
for (final rupees in [500, 350, 300]) {
await app.ledger.recordExpense('ali', Money(rupees * 100), .food);
}
await app.close();
await alerts;
}! Budget alert: Food is at 85% of Rs 1000.00 (Near limit)
! Budget alert: Food is at 115% of Rs 1000.00 (OVER BUDGET)The first expense (50 %) is below the 80 % warning line, so it prints nothing.
Example 8 — The Whole CLI in a Test-Friendly Way
import 'dart:io';
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final dir = await Directory.systemTemp.createTemp('budget_demo_');
final out = StringBuffer();
final err = StringBuffer();
for (final args in [
['demo'],
['spend', 'zayan', '100', 'transport'],
['history', 'bilal'],
['report', 'Oct'],
]) {
final code = await runCli(args, dataDir: dir.path, out: out, err: err);
print('${args.join(' ')} -> exit code $code');
}
stdout.write(out);
stdout.write(err);
await dir.delete(recursive: true);
}demo -> exit code 0
spend zayan 100 transport -> exit code 0
history bilal -> exit code 1
report Oct -> exit code 64
Demo data created for Ali, Sara and Zayan.
Recorded t0007: 2026-10-17 Transport expense Rs 100.00
! Budget alert: Transport is at 140% of Rs 500.00 (OVER BUDGET)
Error: No wallet with id "bilal".
Error: A month looks like 2026-10, not "Oct".The same runCli that bin/ calls, with a temporary folder and StringBuffers — exactly
how test/cli/run_cli_test.dart works.
Common Beginner Mistakes
1. Typing the command right after dart run
$ dart run demo
Could not find package `demo` or file `demo`The first word after dart run is the executable. Write dart run budget_tracker demo.
2. Adding a command but not handling it
Add a DeleteWalletCommand subclass and a parse case, but forget CliRunner, and the
sealed class catches you:
error - lib/src/cli/cli_runner.dart:25:5 - The type 'Command' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'DeleteWalletCommand()'. Try adding a default case or cases that match 'DeleteWalletCommand()'. - non_exhaustive_switch_statementDon't silence it with default — add the case.
3. Bumping the version in one place only
pubspec.yaml and appVersion must both change, or the help text lies.
4. Committing data/
Real spending data is personal. Keep data/ in .gitignore.
5. Expecting the binary to find "its" data
Started from another folder, the binary creates a new, empty data/ there.
Best Practices
- Before a release:
dart format,dart analyze,dart test— all three clean. - Follow semantic versioning: patch for fixes, minor for features, major for breaking changes.
- Depend on interfaces (
Repository<T, Id>), so storage can change without touching services. - Use sealed types for "one of a fixed set".
Mini Challenge
Prove that lib/ works without the CLI. After the session in section 2, write
example/sara_summary.dart that opens data/ and prints Sara's number of transactions and
her balance:
Sara: 3 transactions, Rs 2400.00Try it yourself before opening the solution.
Show solution
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final app = await BudgetApp.open('data');
final ledger = app.ledger;
final history = await ledger.historyOf('sara');
final balance = await ledger.balanceOf('sara');
print('Sara: ${history.length} transactions, ${balance.format()}');
await app.close();
}Sara: 3 transactions, Rs 2400.00close() ends the event stream and closes the activity log, so the program exits cleanly.
Practice Questions
Work through these in order — beginner, then application, then challenge.
Beginner
- Trace
spend ali 200 foodthrough the project: name the file at each step, frombin/to the JSON file. - Point to one use of each in the project: sealed class, extension type, enhanced enum,
record, generic class, extension method,
async*. - Why does
bin/budget_tracker.dartcontain no logic? How could a Flutter app benefit?
Application
-
Build the binary, then run
budget_tracker demofrom a different, empty folder. Where is the data saved, and why? -
You add a new command in a later version. What is the new version number, and what goes into
CHANGELOG.md? -
Write a test (
test/cli/history_test.dart) proving thathistory bilalon an empty data folder returnsexitFailureand printsError: No wallet with id "bilal". -
Write
fastBalances(LedgerService ledger)that computes every wallet's balance in one pass over the transactions, returning aMap<String, Money>. Challenge -
Add a
delete-wallet <wallet>command end to end: help line,Commandsubclass, parse case, runner case, a service rule "only wallets with no transactions", a new exception, and two tests. -
Release that feature: which files change for version 1.1.0, and which commands do you run before calling it done?
Practice Answers
Question 1 — Solution
bin/budget_tracker.dart → cli/run_cli.dart (runCli) → cli/command.dart
(Command.parse → SpendCommand) → budget_app.dart (BudgetApp.open) →
cli/cli_runner.dart (CliRunner.run) → services/ledger_service.dart (recordExpense,
_checkFunds, _record) → repository/json_file_repository.dart (save) →
data/transactions.json. The event then reaches services/budget_monitor.dart (the alert)
and services/activity_log.dart (data/activity.log).
Question 2 — Solution
| Feature | Example |
|---|---|
| sealed class | Transaction, Command, LedgerEvent |
| extension type | Money |
| enhanced enum | Category, BudgetLevel |
| record | CategoryLine, BudgetAlert |
| generic class | Repository<T, Id> |
| extension method | IterableTools<T>, RepositoryQueries |
async* | BudgetMonitor.watch() |
Question 3 — Solution
Logic in bin/ can only run from a terminal and can't be imported or tested. In lib/ it is
a library: tests call runCli with a StringBuffer, and a Flutter app could import
package:budget_tracker/budget_tracker.dart and call LedgerService from its buttons.
Question 4 — Solution
$ /path/to/budget_tracker/build/budget_tracker demo
Demo data created for Ali, Sara and Zayan.
$ ls data
activity.log
budgets.json
transactions.json
wallets.jsondata is a relative path, resolved from the folder the program is started in, not where
the binary lives.
Question 5 — Solution
A new feature that breaks nothing is a minor release: 1.1.0. Add ## 1.1.0 at the top
of CHANGELOG.md with lines like - New command: delete-wallet <wallet>.
Question 6 — Solution
import 'dart:io';
import 'package:budget_tracker/budget_tracker.dart';
import 'package:test/test.dart';
void main() {
test('history of an unknown wallet fails with exit code 1', () async {
final dir = await Directory.systemTemp.createTemp('budget_q_');
final err = StringBuffer();
final code = await runCli(
['history', 'bilal'],
dataDir: dir.path,
out: StringBuffer(),
err: err,
);
expect(code, exitFailure);
expect(err.toString(), 'Error: No wallet with id "bilal".\n');
await dir.delete(recursive: true);
});
}00:00 +1: All tests passed!Question 7 — Solution
example/fast_balances.dart:
import 'package:budget_tracker/budget_tracker.dart';
/// Every balance from one pass over the transactions.
Future<Map<String, Money>> fastBalances(LedgerService ledger) async {
final totals = {
for (final wallet in await ledger.wallets())
wallet.id: wallet.openingBalance,
};
for (final t in await ledger.transactions()) {
final from = totals[t.walletId] ?? Money.zero;
switch (t) {
case Income(:final amount):
totals[t.walletId] = from + amount;
case Expense(:final amount):
totals[t.walletId] = from - amount;
case Transfer(:final amount, :final toWalletId):
totals[t.walletId] = from - amount;
totals[toWalletId] = (totals[toWalletId] ?? Money.zero) + amount;
}
}
return totals;
}
Future<void> main() async {
final app = await BudgetApp.open('data');
final totals = await fastBalances(app.ledger);
totals.forEach((id, balance) => print('$id ${balance.format()}'));
await app.close();
}ali Rs 3100.00
sara Rs 2400.00
zayan Rs 1300.00
bilal Rs 550.00The same numbers as wallets, in wallets + transactions steps instead of wallets ×
transactions.
Question 8 — Solution
app_info.dart — new line in commandHelp:
('delete-wallet', '<wallet>', 'Delete a wallet with no transactions'),command.dart — a parse case and a subclass:
['delete-wallet', final wallet] => DeleteWalletCommand(wallet),final class DeleteWalletCommand extends Command {
const DeleteWalletCommand(this.walletId);
final String walletId;
}errors.dart:
/// Thrown when a wallet that still has transactions is deleted.
final class WalletInUseException extends BudgetException {
WalletInUseException(this.walletId)
: super('Wallet "$walletId" has transactions and cannot be deleted.');
final String walletId;
}ledger_service.dart:
/// Deletes a wallet that has no transactions.
Future<void> deleteWallet(String walletId) async {
if ((await historyOf(walletId)).isNotEmpty) {
throw WalletInUseException(walletId);
}
await _wallets.delete(walletId);
}cli_runner.dart — the case the analyzer asked for:
case DeleteWalletCommand(:final walletId):
await ledger.deleteWallet(walletId);
_out.writeln('Deleted wallet "$walletId".');test/cli/delete_wallet_test.dart:
import 'package:budget_tracker/budget_tracker.dart';
import 'package:test/test.dart';
import '../helpers/test_app.dart';
void main() {
test('a wallet without transactions can be deleted', () async {
final app = await seededApp();
await app.ledger.deleteWallet('sara');
expect(await app.ledger.wallets(), hasLength(1));
});
test('a wallet with transactions cannot be deleted', () async {
final app = await seededApp();
expect(
app.ledger.deleteWallet('ali'),
throwsA(isA<WalletInUseException>()),
);
});
}$ dart test
00:00 +55: All tests passed!
$ dart run budget_tracker add-wallet bilal Bilal "Piggy bank"
Opened wallet "bilal" for Bilal.
$ dart run budget_tracker delete-wallet bilal
Deleted wallet "bilal".
$ dart run budget_tracker delete-wallet ali; echo "exit code: $?"
Error: Wallet "ali" has transactions and cannot be deleted.
exit code: 1WalletInUseException must live in errors.dart, the sealed base's library.
Question 9 — Solution
Change version: 1.1.0 in pubspec.yaml, appVersion = '1.1.0' in app_info.dart, add a
## 1.1.0 CHANGELOG entry, and add the command to the README table. Then run
dart format ., dart analyze (no issues), dart test (all pass), try the command by hand,
and rebuild with dart compile exe.
Quick Revision
- In which direction do the project's layers depend on each other?
- What does
executables:inpubspec.yamldeclare? - Why does
version: 1.0.0matter more than0.9.0? - Which two classes listen to
ledger.events? - Why is
Moneyan extension type overintinstead of adouble? - What do exit codes 0, 1 and 64 mean in this app?
- What does
dart compile exegive you thatdart rundoes not?
Key Takeaways
- A release is a version, a CHANGELOG entry, a README and three clean checks.
- The project flows one way:
bin → cli → services → repository → models. - Composition (
BudgetApp), interfaces (Repository<T, Id>) and sealed types carry the design. - Every course category appears somewhere in about 1,500 lines of code.
dart compile exebuilds a native program; relative paths follow the working folder.- A good review names the weak spots too — and the logic in
lib/is ready for any front end.
What's Next?
This is the end of the Dart Complete Course. You started with print('Hello') and
finished with a tested, analyzed, versioned application that uses classes, generics,
patterns, sealed types, streams, files and JSON.
Where to go from here:
- Flutter. The course deliberately left Flutter out, so the language could have your full
attention. Flutter is Dart for mobile, web and desktop interfaces — and everything here,
from null safety to async code, is what Flutter apps are made of. A good first project:
a Flutter screen on top of this project's
lib/. - Effective Dart (dart.dev/effective-dart). The official guide to style, documentation, usage and design. Reread it now — after this project, most rules will feel obvious.
- pub.dev. Thousands of packages. Try
package:argsto add flags to the budget tracker, and read popular packages' source to see how others organize code.
Keep building small programs — and congratulations on finishing the course.