On this page27 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Where We Start
- 2. A Stricter analysis_options.yaml
- 3. Let dart fix Do the Mechanical Work
- 4. Review What the Tool Changed
- 5. The Missing await
- 6. Three Bugs the Tests Never Saw
- 7. The Plan: Parse, Run, Exit
- 8. A Sealed Command with List Patterns
- 9. One Usage Table, One Usage Exception
- 10. CliRunner: an Exhaustive Switch That Writes to a Sink
- 11. runCli and a Seven-Line bin/
- 12. Demo Data with Dot Shorthands
- 13. One Generic Helper Instead of Three Copies
- 14. New Tests for the CLI
- 15. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 139 — Analysis & Refactoring
| Category | 23 — Final Dart Project |
| Lecture | 139 of 140 |
| Estimated video length | 15 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
Forty tests pass, yet
report Octoberstill crashes, because all the command-line logic sits inside one 200-linemainthat no test can call. A stricter analyzer,dart fixand a refactor into a sealedCommandtype make that code checkable, and fix three real bugs.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.
Prerequisites
Students should already understand, from Lectures 073–074, 079–082, 113–117, 123, 127, 129 and 132–138:
- the analyzer,
analysis_options.yaml, lints, severities anddart fix - list patterns with rest elements, object patterns, sealed classes and exhaustive
switch - generic methods with a bound (
<T extends ...>) and dot shorthands (.food) - exit codes, and the Budget Tracker with its 40 tests from Lecture 138
Connection With Previous Lectures
At the end of Lecture 138, dart test printed +40: All tests passed!. But not one of those
tests touches bin/budget_tracker.dart. Its main parses the arguments, calls the services,
prints with print, and sets exitCode itself, all in one function. To test it you would
have to start the whole program. So the bugs in it survive: a multi-word note loses words,
report October crashes, and a broken data file prints a stack trace.
Learning Objectives
By the end of this lecture you will be able to:
- Turn on a stricter analysis profile and read what it reports
- Let
dart fixapply the mechanical fixes, then review them by hand - Explain why a missing
awaitcan "work" and still be a bug - Replace a long
if/elsechain with a sealedCommandtype and list patterns - Make output testable by writing to a
StringSinkinstead of callingprint - Remove duplicated code with a generic helper
_record<T extends Transaction> - Prove the refactor with new tests,
dart analyzeand the same terminal session
1. Where We Start
Refactoring changes how code is written, never what it does (Lecture 123). We start from
the Lecture 138 project and change only analysis_options.yaml.
Analogy — refactoring is rewiring a house while people live in it: every light must still work, so you test each switch as you go. Ours are
dart analyze,dart testand one terminal session run before and after.
2. A Stricter analysis_options.yaml
Replace the file in the project root:
include: package:lints/recommended.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
unawaited_futures: warning
linter:
rules:
- prefer_final_locals
- prefer_const_constructors
- unawaited_futures
- avoid_redundant_argument_values
- directives_ordering
- unnecessary_lambdasThe three strict- modes (Lecture 114) stop hidden dynamic. The six rules (Lecture 117) ask
for final, const, sorted imports and tear-offs. errors: raises unawaited_futures from
info to warning, so a forgotten await fails the run:
$ dart analyze
Analyzing budget_tracker...
warning - bin/budget_tracker.dart:194:15 - Missing an 'await' for the 'Future' computed by this expression. Try adding an 'await' or wrapping the expression with 'unawaited'. - unawaited_futures
info - bin/budget_tracker.dart:11:3 - Local variables should be final. Try making the variable final. - prefer_final_locals
...
52 issues found.(Shortened.) The exit code is 2, because one issue is a warning. Most issues are
prefer_final_locals, which even flags pattern variables in the models' fromJson patterns.
3. Let dart fix Do the Mechanical Work
Fifty-one issues have a known fix. dart fix --dry-run previews them (51 proposed fixes in 9 files., listed per file and rule) without touching anything. Then apply them:
$ dart fix --apply
...
51 fixes made in 9 files.Run dart analyze again: only the warning at line 194 is left, and it ends 1 issue found.
4. Review What the Tool Changed
Automatic fixes still need a reviewer (Lecture 127). Pattern variables became
'id': final String id, and DateTime(now.year, now.month, 1) lost its default 1: correct,
but less readable. And look at this:
var opening = Money.zero;
if (args.length > 4) {
opening = Money.tryParse(args[4]) ?? const Money(-1);
if (opening == const Money(-1)) throw InvalidAmountException(args[4]);
}dart fix happily made the Money(-1) sentinel const. No tool tells you that "−1 rupees
means invalid" is a design smell; that is our job.
5. The Missing await
Line 194 is in the demo branch:
await budgets.setBudget(Category.transport, const Money(50000));
budgets.setBudget(Category.fun, const Money(100000));The demo still saved the Fun budget, because main went on to await app.close() and the
save finished in the meantime. That is luck, not design: if the save failed, the error would
escape every try. Lecture 089 warned about exactly this.
6. Three Bugs the Tests Never Saw
Run the "before" program (sessions here ran on 2026-10-17; yours show today's date):
$ dart run budget_tracker demo
Demo data created for Ali, Sara and Zayan.
$ dart run budget_tracker spend ali 200 food pizza with friends
Recorded t0007: 2026-10-17 Food expense (pizza) Rs 200.00
! Budget alert: Food is at 93% of Rs 1500.00 (Near limit)
$ dart run budget_tracker report October
Unhandled exception:
FormatException: Invalid date format
October-01
#0 DateTime.parse (dart:core/date_time.dart:381:7)
#1 main (file:///path/to/budget_tracker/bin/budget_tracker.dart:107:45)
<asynchronous suspension>
$ echo "oops" > data/budgets.json
$ dart run budget_tracker report; echo "exit code: $?"
Unhandled exception:
Could not read "data/budgets.json": Unexpected character
...
#4 main (file:///path/to/budget_tracker/bin/budget_tracker.dart:11:13)
<asynchronous suspension>
exit code: 255note: args.length > 4 ? args[4] : ''keeps onlypizza.DateTime.parsethrows aFormatException, which is not aBudgetException.BudgetApp.openruns before thetry, so itsStorageExceptionis never caught.
7. The Plan: Parse, Run, Exit
We split main into testable pieces in a new folder lib/src/cli/:
| File | Job |
|---|---|
command.dart | List<String> → a checked Command |
cli_runner.dart | run a Command, write text to a StringSink |
run_cli.dart | open the app, catch errors, return an exit code |
demo_data.dart | the demo wallets and transactions |
Export the first three from lib/budget_tracker.dart:
export 'src/cli/cli_runner.dart';
export 'src/cli/command.dart';
export 'src/cli/run_cli.dart';8. A Sealed Command with List Patterns
lib/src/cli/command.dart starts with a sealed base whose factory is one switch over list
patterns (excerpt; the other commands follow the same shape):
sealed class Command {
const Command();
factory Command.parse(List<String> args) => switch (args) {
[] || ['help'] => const HelpCommand(),
['wallets'] => const WalletsCommand(),
// ... add-wallet, income ...
['spend', final wallet, final amount, final category, ...final note] =>
SpendCommand(
wallet,
_amount(amount),
_category(category),
note.join(' '),
),
// ... transfer, budget ...
['report'] => const ReportCommand(),
['report', final month] => ReportCommand(month: _month(month)),
// ... report with wallet, history, demo ...
[final name, ...] => throw UsageException(_usageOf(name)),
};
static Money _amount(String text) =>
Money.tryParse(text) ?? (throw InvalidAmountException(text));
static DateTime _month(String text) =>
DateTime.tryParse('$text-01') ??
(throw UsageException('A month looks like 2026-10, not "$text".'));...final note collects every remaining word (bug 1). _month uses tryParse and throws our
own exception (bug 2). ?? (throw ...) replaces the sentinel. Anything else hits the last case.
Ten final class subclasses follow, each a const constructor plus final fields:
final class SpendCommand extends Command {
const SpendCommand(this.walletId, this.amount, this.category, this.note);
final String walletId;
final Money amount;
final Category category;
final String note;
}The rest: HelpCommand, WalletsCommand, DemoCommand (no fields),
AddWalletCommand(id, owner, name, opening), IncomeCommand(walletId, amount, source),
TransferCommand(fromId, toId, amount), BudgetCommand(category, limit),
ReportCommand({month, walletId}) (nullable) and HistoryCommand(walletId).
9. One Usage Table, One Usage Exception
The old main repeated print('Usage: ...'); exitCode = 64; seven times. Now
lib/src/app_info.dart lists each command once (renamed from plannedCommands):
const commandHelp = [
('wallets', '', 'List every wallet with its balance'),
('add-wallet', '<id> <owner> <name> [opening]', 'Open a new wallet'),
// ... one record per command ...
('demo', '', 'Fill an empty data folder with sample data'),
];_usageOf(name) in command.dart builds Usage: transfer <from> <to> <amount> from it. The
new exception must go into lib/src/errors.dart, because a sealed class can only be
extended in its own library:
final class UsageException extends BudgetException {
const UsageException(super.message);
}10. CliRunner: an Exhaustive Switch That Writes to a Sink
lib/src/cli/cli_runner.dart holds the old if branches, moved, with print replaced by
_out.writeln (excerpt):
Future<void> run(Command command) async {
final ledger = _app.ledger;
switch (command) {
case HelpCommand():
writeHelp(_out);
// ... wallets, add-wallet, income ...
case SpendCommand(
:final walletId,
:final amount,
:final category,
:final note,
):
_recorded(
await ledger.recordExpense(walletId, amount, category, note: note),
);
// ... transfer, budget, report, history, demo ...
}
}stdout and StringBuffer are both StringSinks, so a test can read every line. The file
also holds writeHelp(StringSink out) and formatAlert(BudgetAlert alert). Command is
sealed, so the switch needs no default, and a forgotten new subclass stops the compiler:
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 'RenameWalletCommand()'. Try adding a default case or cases that match 'RenameWalletCommand()'. - non_exhaustive_switch_statement11. runCli and a Seven-Line bin/
lib/src/cli/run_cli.dart is the whole program as a function that returns its exit code:
const exitOk = 0;
const exitFailure = 1;
const exitUsage = 64;
Future<int> runCli(
List<String> args, {
String dataDir = 'data',
StringSink? out,
StringSink? err,
DateTime Function()? clock,
}) async {
out ??= stdout;
err ??= stderr;
try {
final command = Command.parse(args);
if (command is HelpCommand) {
writeHelp(out);
return exitOk;
}
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;
}
}(Imports and doc comments left out.) BudgetApp.open is now inside the try, which fixes
bug 3, and named constants replace the magic numbers.
bin/budget_tracker.dart shrinks from 217 lines to seven:
import 'dart:io';
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}12. Demo Data with Dot Shorthands
The demo branch moves to lib/src/cli/demo_data.dart as
Future<void> seedDemo(BudgetApp app, DateTime today). Every call is awaited, so the
unawaited-futures warning is gone. Its last lines:
await app.budgets.setBudget(.food, const Money(150000));
await app.budgets.setBudget(.transport, const Money(50000));
await app.budgets.setBudget(.fun, const Money(100000));The parameter type is Category, so .food is enough (Lecture 129). That leaves the
Category import unused, and the analyzer says so:
warning - lib/src/cli/demo_data.dart:2:8 - Unused import: '../models/category.dart'. Try removing the import directive. - unused_importDelete the import. lib/src/services/budget_monitor.dart gets the same treatment:
if (level == .warning || level == .over) {.
13. One Generic Helper Instead of Three Copies
In lib/src/services/ledger_service.dart, recordIncome, recordExpense and transfer each
ended with the same three lines: save, announce, return. They become one private helper:
/// 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;
}Each method now ends with return _record(Income(...));. The bound lets the helper treat T
as a Transaction; T keeps the exact type, so recordIncome still returns
Future<Income>. A non-generic _record(Transaction transaction) fails three times:
error - lib/src/services/ledger_service.dart:93:12 - A value of type 'Future<Transaction>' can't be returned from the method 'recordIncome' because it has a return type of 'Future<Income>'. - return_of_invalid_type14. New Tests for the CLI
Now the CLI is testable. test/cli/command_test.dart (8 tests) pins down the note bug:
test('spend reads the amount, the category and a multi-word note', () {
final command = Command.parse([
'spend',
'ali',
'12.50',
'food',
'hot',
'tea',
]);
expect(
command,
isA<SpendCommand>()
.having((c) => c.walletId, 'walletId', 'ali')
.having((c) => c.amount, 'amount', const Money(1250))
.having((c) => c.category, 'category', Category.food)
.having((c) => c.note, 'note', 'hot tea'),
);
});test/cli/run_cli_test.dart (5 tests) calls runCli through a helper run(args) with a temp
folder, two StringBuffers and testToday, checking alerts, exit codes and a broken file.
$ dart test
00:00 +53: All tests passed!15. A Real-World Example
The same session as section 6, on the refactored project:
$ dart run budget_tracker demo
Demo data created for Ali, Sara and Zayan.
$ dart run budget_tracker spend ali 200 food pizza with friends
Recorded t0007: 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 report October
Error: A month looks like 2026-10, not "October".
$ echo "oops" > data/budgets.json
$ dart run budget_tracker report; echo "exit code: $?"
Error: Could not read "data/budgets.json": Unexpected character
exit code: 1
$ dart analyze
Analyzing budget_tracker...
No issues found!Working commands print exactly what they printed before; the three bugs are gone.
Code Examples
These programs use the refactored project. Put each in a new example/ folder in the
project and run it with dart run example/<file>.dart. The finished project has no
example/ folder, so delete it afterwards.
Example 1 — dart fix on a Small File
example/allowance.dart:
import 'package:budget_tracker/budget_tracker.dart';
void main() {
var allowance = Money(500000);
var gift = Money(25000);
print((allowance + gift).format());
}$ dart analyze example
Analyzing example...
info - allowance.dart:4:3 - Local variables should be final. Try making the variable final. - prefer_final_locals
...
4 issues found.
$ dart fix --apply example
...
4 fixes made in 1 file.
$ dart run example/allowance.dart
Rs 5250.00The fix writes final allowance = const Money(500000);. A reviewer would write
const allowance = Money(500000);.
Example 2 — Parsing a Command
import 'package:budget_tracker/budget_tracker.dart';
void main() {
final command = Command.parse([
'spend',
'zayan',
'45',
'transport',
'bus',
'home',
]);
if (command case SpendCommand(:final amount, :final category, :final note)) {
print('${category.label}: ${amount.format()} ($note)');
}
print(Command.parse(['report']).runtimeType);
print(Command.parse([]).runtimeType);
}Transport: Rs 45.00 (bus home)
ReportCommand
HelpCommandExample 3 — Every Kind of Usage Error
import 'package:budget_tracker/budget_tracker.dart';
void main() {
const attempts = [
['income', 'sara'],
['spend', 'ali', '50', 'snacks'],
['report', '10-2026'],
['spend', 'ali', 'fifty', 'food'],
];
for (final args in attempts) {
try {
Command.parse(args);
} on BudgetException catch (e) {
print('${e.runtimeType}: $e');
}
}
}UsageException: Usage: income <wallet> <amount> <source>
UsageException: Unknown category "snacks". Choose one of: food, transport, books, rent, fun, other.
UsageException: A month looks like 2026-10, not "10-2026".
InvalidAmountException: "fifty" is not a valid amount. Use a positive number like 250.Example 4 — Help into a StringBuffer
import 'package:budget_tracker/budget_tracker.dart';
void main() {
final buffer = StringBuffer();
writeHelp(buffer);
final lines = buffer.toString().trim().split('\n');
print('${lines.length} lines');
print(lines.first);
print(lines.last.trim());
}12 lines
Student Budget Tracker v0.1.0
demo Fill an empty data folder with sample dataExample 5 — _record<T> Keeps the Type
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final app = BudgetApp.inMemory(clock: () => DateTime(2026, 10, 17));
final ledger = app.ledger;
await ledger.openWallet(id: 'sara', owner: 'Sara', name: 'Bank card');
final income = await ledger.recordIncome(
'sara',
const Money(150000),
source: 'Scholarship',
);
final expense = await ledger.recordExpense(
'sara',
const Money(40000),
.books,
);
print('${income.id} came from ${income.source}');
print('${expense.id} was spent on ${expense.category.label}');
await app.close();
}t0001 came from Scholarship
t0002 was spent on Booksincome.source compiles only because recordIncome still returns an Income.
Example 6 — Dot Shorthands with Budget Levels
import 'package:budget_tracker/budget_tracker.dart';
void main() {
const limit = Money(150000);
for (final spent in [
const Money(60000),
const Money(140000),
const Money(160000),
]) {
final level = BudgetLevel.of(spent, limit);
final alert = level == .warning || level == .over;
print('${spent.format()} -> ${level.label}${alert ? ' (alert)' : ''}');
}
}Rs 600.00 -> OK
Rs 1400.00 -> Near limit (alert)
Rs 1600.00 -> OVER BUDGET (alert)Example 7 — Exit Codes from a Sealed Exception
import 'package:budget_tracker/budget_tracker.dart';
int exitCodeFor(BudgetException error) => switch (error) {
UsageException() => exitUsage,
WalletNotFoundException() ||
DuplicateWalletException() ||
InvalidAmountException() ||
StorageException() ||
InsufficientFundsException() => exitFailure,
};
void main() {
print(exitCodeFor(const UsageException('Usage: history <wallet>')));
print(exitCodeFor(WalletNotFoundException('bilal')));
print(exitCodeFor(InvalidAmountException('lots')));
}64
1
1Example 8 — A Whole Session Inside One Program
import 'dart:io';
import 'package:budget_tracker/budget_tracker.dart';
Future<void> main() async {
final dir = await Directory.systemTemp.createTemp('budget_example_');
final out = StringBuffer();
final err = StringBuffer();
Future<void> run(List<String> args) async {
final code = await runCli(args, dataDir: dir.path, out: out, err: err);
print('${args.join(' ')} -> exit $code');
}
await run(['demo']);
await run(['spend', 'zayan', '5000', 'fun']);
await run(['history']);
stdout.write(out);
stdout.write(err);
await dir.delete(recursive: true);
}demo -> exit 0
spend zayan 5000 fun -> exit 1
history -> exit 64
Demo data created for Ali, Sara and Zayan.
Error: Wallet "zayan" has only Rs 1400.00, cannot take Rs 5000.00.
Error: Usage: history <wallet>Common Beginner Mistakes
1. Putting the catch-all case first
// [] || ['help'] => const HelpCommand(),
// [final name, ...] => throw UsageException(_usageOf(name)), // ✗ moved upwarning - lib/src/cli/command.dart:17:17 - This case is covered by the previous cases. Try removing the case clause, or restructuring the preceding patterns. - unreachable_switch_caseCases are tried top to bottom. [final name, ...] matches every non-empty list, so it goes last.
2. Declaring UsageException in another file
// lib/src/cli/usage_error.dart
// final class UsageError extends BudgetException { ... } // ✗ error - lib/src/cli/usage_error.dart:3:32 - The class 'BudgetException' can't be extended, implemented, or mixed in outside of its library because it's a sealed class. - invalid_use_of_type_outside_library3. Running dart run demo
$ dart run demo
Could not find package `demo` or file `demo`The first word after dart run names the program. Write dart run budget_tracker demo.
4. Trusting dart fix blindly. It made the Money(-1) sentinel const. Read the diff.
Best Practices
- Raise rules that catch real bugs, like
unawaited_futures, towarning. - Run
dart fix --dry-runfirst, then review what--applychanged. - Keep
maintiny. Put logic in functions that take their inputs and return results. - Write output to a
StringSinkso tests can capture it. - Name exit codes; never scatter
64through the code. - Prefer
tryParsewith?? (throw ...)over sentinels and uncaughtparseerrors.
Mini Challenge
Add a test to test/cli/run_cli_test.dart proving that report October returns
exitUsage instead of crashing. Use the file's run helper.
Try it yourself before opening the solution.
Show solution
test('a badly written month is a usage error', () async {
expect(await run(['report', 'October']), exitUsage);
expect(err.toString(), startsWith('Error: A month looks like'));
});$ dart test test/cli/run_cli_test.dart
00:00 +6: All tests passed!Against the "before" code no such test could exist: there was no function to call.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Parse
['history', 'sara']and printHistory of sara. - Parse
['report', '2026-09']and printReport for 2026-09. - Catch the
UsageExceptionfromCommand.parse(['budget'])and print its message.
Application
-
dart analyzereportsunnecessary_lambdasfor this program. Fix it:void main() { final amounts = [10000, 25050].map((cents) => Money(cents)); print(amounts.map((m) => m.format()).join(', ')); } -
Find the warning in this program and fix it:
Future<void> main() async { final app = BudgetApp.inMemory(); app.budgets.setBudget(.food, const Money(150000)); print('Food budget saved'); await app.close(); } -
Call
formatAlertwith aBudgetAlertrecord for Fun: spent Rs 900, limit Rs 1000, levelwarning. Use dot shorthands for the category and the level.
Challenge
- In a copy of the project, add
balance <wallet>, which printssara has Rs 1200.00afterdemo. Let the compiler guide you. - Add
public_member_api_docsto the rules list and count the missing doc comments. Which file has the most?
Practice Answers
Each program starts with import 'package:budget_tracker/budget_tracker.dart';.
Question 1 — Solution
void main() {
final command = Command.parse(['history', 'sara']);
if (command case HistoryCommand(:final walletId)) {
print('History of $walletId');
}
}History of saraQuestion 2 — Solution
void main() {
final command = Command.parse(['report', '2026-09']);
if (command case ReportCommand(:final month)) {
print('Report for ${month?.monthLabel}');
}
}Report for 2026-09Question 3 — Solution
void main() {
try {
Command.parse(['budget']);
} on UsageException catch (e) {
print(e.message);
}
}Usage: budget <category> <limit>Question 4 — Solution
info - q4.dart:4:38 - Closure should be a tearoff. Try using a tearoff rather than a closure. - unnecessary_lambdasvoid main() {
final amounts = [10000, 25050].map(Money.new);
print(amounts.map((m) => m.format()).join(', '));
}Rs 100.00, Rs 250.50(m) => m.format() stays: it calls a method on its argument, so no tear-off fits.
Question 5 — Solution
warning - q5.dart:5:15 - Missing an 'await' for the 'Future' computed by this expression. Try adding an 'await' or wrapping the expression with 'unawaited'. - unawaited_futuresFuture<void> main() async {
final app = BudgetApp.inMemory();
await app.budgets.setBudget(.food, const Money(150000));
print('Food budget saved');
await app.close();
}Food budget savedQuestion 6 — Solution
void main() {
final alert = formatAlert((
category: .fun,
spent: const Money(90000),
limit: const Money(100000),
level: .warning,
));
print(alert);
}! Budget alert: Fun is at 90% of Rs 1000.00 (Near limit)The parameter type BudgetAlert gives each field its context type.
Question 7 — Solution
In command.dart, add a case before [final name, ...] and a subclass:
['balance', final wallet] => BalanceCommand(wallet),final class BalanceCommand extends Command {
const BalanceCommand(this.walletId);
final String walletId;
}dart analyze now reports non_exhaustive_switch_statement at cli_runner.dart:25:5, the
same error as in section 10 with 'BalanceCommand()'. Add the case in cli_runner.dart (and a commandHelp row for the usage line):
case BalanceCommand(:final walletId):
final balance = await ledger.balanceOf(walletId);
_out.writeln('$walletId has ${balance.format()}');$ dart run budget_tracker balance sara
sara has Rs 1200.00Question 8 — Solution
- public_member_api_docs$ dart analyze
...
99 issues found.All 99 are public_member_api_docs. lib/src/cli/command.dart has the most (37): the ten
command classes, their constructors and their fields. The finished project keeps this lint off.
Quick Revision
- What does
errors: unawaited_futures: warningchange? - Why run
dart fix --dry-runbefore--apply? - Which part of
Command.parsefixed the multi-word note? - Why must
UsageExceptionlive inerrors.dart? - Why does
CliRunnertake aStringSinkinstead of callingprint? - What does the bound in
_record<T extends Transaction>allow, and what doesTpreserve?
Key Takeaways
- Stricter analysis turns hidden risks into a list you can work through.
dart fixhandles mechanical issues; design smells still need a human.- Logic locked inside
maincannot be tested, so its bugs survive. - A sealed
Commandwith list patterns parses arguments in oneswitch. - An exhaustive
switchshows every place a new command must be handled. - A
StringSinkand a returned exit code make the whole CLI testable. - A bounded generic helper removes duplication and keeps exact types.
- Same output, 53 passing tests,
No issues found!: the refactor is proven.
What's Next?
Lecture 140 — Final Project Review. The code is clean, analyzed and tested. Next we walk
the whole architecture from bin/ down to the models, map each part to the lectures it came
from, release version 1.0.0 with a README and CHANGELOG, and compile a native executable.