Watch the video (15 min)
On this page27 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Where We Start
  5. 2. A Stricter analysis_options.yaml
  6. 3. Let dart fix Do the Mechanical Work
  7. 4. Review What the Tool Changed
  8. 5. The Missing await
  9. 6. Three Bugs the Tests Never Saw
  10. 7. The Plan: Parse, Run, Exit
  11. 8. A Sealed Command with List Patterns
  12. 9. One Usage Table, One Usage Exception
  13. 10. CliRunner: an Exhaustive Switch That Writes to a Sink
  14. 11. runCli and a Seven-Line bin/
  15. 12. Demo Data with Dot Shorthands
  16. 13. One Generic Helper Instead of Three Copies
  17. 14. New Tests for the CLI
  18. 15. A Real-World Example
  19. Code Examples
  20. Common Beginner Mistakes
  21. Best Practices
  22. Mini Challenge
  23. Practice Questions
  24. Practice Answers
  25. Quick Revision
  26. Key Takeaways
  27. What's Next?

Lecture 139 — Analysis & Refactoring

Category23 — Final Dart Project
Lecture139 of 140
Estimated video length15 min
TargetDart 3.12.2
LevelAdvanced

Forty tests pass, yet report October still crashes, because all the command-line logic sits inside one 200-line main that no test can call. A stricter analyzer, dart fix and a refactor into a sealed Command type 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 and dart 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 fix apply the mechanical fixes, then review them by hand
  • Explain why a missing await can "work" and still be a bug
  • Replace a long if/else chain with a sealed Command type and list patterns
  • Make output testable by writing to a StringSink instead of calling print
  • Remove duplicated code with a generic helper _record<T extends Transaction>
  • Prove the refactor with new tests, dart analyze and 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 test and one terminal session run before and after.


2. A Stricter analysis_options.yaml

Replace the file in the project root:

yaml
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_lambdas

The 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:

output
$ 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:

output
$ 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:

dart
      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:

dart
      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):

output
$ 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: 255
  1. note: args.length > 4 ? args[4] : '' keeps only pizza.
  2. DateTime.parse throws a FormatException, which is not a BudgetException.
  3. BudgetApp.open runs before the try, so its StorageException is never caught.

7. The Plan: Parse, Run, Exit

We split main into testable pieces in a new folder lib/src/cli/:

FileJob
command.dartList<String> → a checked Command
cli_runner.dartrun a Command, write text to a StringSink
run_cli.dartopen the app, catch errors, return an exit code
demo_data.dartthe demo wallets and transactions

Export the first three from lib/budget_tracker.dart:

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):

dart
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:

dart
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):

dart
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:

dart
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):

dart
  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:

output
  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_statement

11. runCli and a Seven-Line bin/

lib/src/cli/run_cli.dart is the whole program as a function that returns its exit code:

dart
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:

dart
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:

dart
  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:

output
warning - lib/src/cli/demo_data.dart:2:8 - Unused import: '../models/category.dart'. Try removing the import directive. - unused_import

Delete 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:

dart
  /// 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:

output
  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_type

14. New Tests for the CLI

Now the CLI is testable. test/cli/command_test.dart (8 tests) pins down the note bug:

dart
  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.

output
$ dart test
00:00 +53: All tests passed!

15. A Real-World Example

The same session as section 6, on the refactored project:

output
$ 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:

dart
import 'package:budget_tracker/budget_tracker.dart';

void main() {
  var allowance = Money(500000);
  var gift = Money(25000);
  print((allowance + gift).format());
}
output
$ 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.00

The fix writes final allowance = const Money(500000);. A reviewer would write const allowance = Money(500000);.

Example 2 — Parsing a Command

dart
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);
}
output
Transport: Rs 45.00 (bus home)
ReportCommand
HelpCommand

Example 3 — Every Kind of Usage Error

dart
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');
    }
  }
}
output
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

dart
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());
}
output
12 lines
Student Budget Tracker v0.1.0
demo                                       Fill an empty data folder with sample data

Example 5 — _record<T> Keeps the Type

dart
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();
}
output
t0001 came from Scholarship
t0002 was spent on Books

income.source compiles only because recordIncome still returns an Income.

Example 6 — Dot Shorthands with Budget Levels

dart
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)' : ''}');
  }
}
output
Rs 600.00 -> OK
Rs 1400.00 -> Near limit  (alert)
Rs 1600.00 -> OVER BUDGET  (alert)

Example 7 — Exit Codes from a Sealed Exception

dart
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')));
}
output
64
1
1

Example 8 — A Whole Session Inside One Program

dart
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);
}
output
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

dart
//   [] || ['help'] => const HelpCommand(),
//   [final name, ...] => throw UsageException(_usageOf(name)), // ✗ moved up
output
warning - 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_case

Cases are tried top to bottom. [final name, ...] matches every non-empty list, so it goes last.

2. Declaring UsageException in another file

dart
// lib/src/cli/usage_error.dart
// final class UsageError extends BudgetException { ... } // ✗
output
  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_library

3. Running dart run demo

output
$ 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, to warning.
  • Run dart fix --dry-run first, then review what --apply changed.
  • Keep main tiny. Put logic in functions that take their inputs and return results.
  • Write output to a StringSink so tests can capture it.
  • Name exit codes; never scatter 64 through the code.
  • Prefer tryParse with ?? (throw ...) over sentinels and uncaught parse errors.

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
dart
  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'));
  });
output
$ 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

  1. Parse ['history', 'sara'] and print History of sara.
  2. Parse ['report', '2026-09'] and print Report for 2026-09.
  3. Catch the UsageException from Command.parse(['budget']) and print its message.

Application

  1. dart analyze reports unnecessary_lambdas for this program. Fix it:

    dart
    void main() {
      final amounts = [10000, 25050].map((cents) => Money(cents));
      print(amounts.map((m) => m.format()).join(', '));
    }
  2. Find the warning in this program and fix it:

    dart
    Future<void> main() async {
      final app = BudgetApp.inMemory();
      app.budgets.setBudget(.food, const Money(150000));
      print('Food budget saved');
      await app.close();
    }
  3. Call formatAlert with a BudgetAlert record for Fun: spent Rs 900, limit Rs 1000, level warning. Use dot shorthands for the category and the level.

Challenge

  1. In a copy of the project, add balance <wallet>, which prints sara has Rs 1200.00 after demo. Let the compiler guide you.
  2. Add public_member_api_docs to 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

dart
void main() {
  final command = Command.parse(['history', 'sara']);
  if (command case HistoryCommand(:final walletId)) {
    print('History of $walletId');
  }
}
output
History of sara

Question 2 — Solution

dart
void main() {
  final command = Command.parse(['report', '2026-09']);
  if (command case ReportCommand(:final month)) {
    print('Report for ${month?.monthLabel}');
  }
}
output
Report for 2026-09

Question 3 — Solution

dart
void main() {
  try {
    Command.parse(['budget']);
  } on UsageException catch (e) {
    print(e.message);
  }
}
output
Usage: budget <category> <limit>

Question 4 — Solution

output
   info - q4.dart:4:38 - Closure should be a tearoff. Try using a tearoff rather than a closure. - unnecessary_lambdas
dart
void main() {
  final amounts = [10000, 25050].map(Money.new);
  print(amounts.map((m) => m.format()).join(', '));
}
output
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

output
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_futures
dart
Future<void> main() async {
  final app = BudgetApp.inMemory();
  await app.budgets.setBudget(.food, const Money(150000));
  print('Food budget saved');
  await app.close();
}
output
Food budget saved

Question 6 — Solution

dart
void main() {
  final alert = formatAlert((
    category: .fun,
    spent: const Money(90000),
    limit: const Money(100000),
    level: .warning,
  ));
  print(alert);
}
output
! 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:

dart
    ['balance', final wallet] => BalanceCommand(wallet),
dart
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):

dart
      case BalanceCommand(:final walletId):
        final balance = await ledger.balanceOf(walletId);
        _out.writeln('$walletId has ${balance.format()}');
output
$ dart run budget_tracker balance sara
sara has Rs 1200.00

Question 8 — Solution

yaml
    - public_member_api_docs
output
$ 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

  1. What does errors: unawaited_futures: warning change?
  2. Why run dart fix --dry-run before --apply?
  3. Which part of Command.parse fixed the multi-word note?
  4. Why must UsageException live in errors.dart?
  5. Why does CliRunner take a StringSink instead of calling print?
  6. What does the bound in _record<T extends Transaction> allow, and what does T preserve?

Key Takeaways

  • Stricter analysis turns hidden risks into a list you can work through.
  • dart fix handles mechanical issues; design smells still need a human.
  • Logic locked inside main cannot be tested, so its bugs survive.
  • A sealed Command with list patterns parses arguments in one switch.
  • An exhaustive switch shows every place a new command must be handled.
  • A StringSink and 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.