Watch the video (25 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Releasing Version 1.0.0
  5. 2. The Full Session
  6. 3. Layer 1 — bin/ and the CLI
  7. 4. Layer 2 — BudgetApp Wires Everything
  8. 5. Layer 3 — Services Enforce the Rules
  9. 6. Layer 4 — Repositories and the Files on Disk
  10. 7. Layer 5 — The Models
  11. 8. The Concept Map
  12. 9. The Quality Checks
  13. 10. Compiling with dart compile exe
  14. 11. An Honest Review
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 140 — Final Project Review

Category23 — Final Dart Project
Lecture140 of 140
Estimated video length25 min
TargetDart 3.12.2
LevelAdvanced

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 demo command 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 exe and 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:

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

dart
/// The current version of the app.
const appVersion = '1.0.0';

.gitignore — each user's money data is not source code:

output
# The app's own data folder
data/

CHANGELOG.md — newest version first (Lecture 125):

markdown
## 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:

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

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

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

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

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

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

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;
  }

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

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:

output
[
  {
    "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:

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

dart
    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)ConceptsLectures
models/wallet.dart, budget.dartfinal class, const and factory constructors, copyWith, ==/hashCode053–056, 124, 130
models/transaction.dartsealed class, super parameters, map patterns in fromJson059, 061, 080, 082
models/money.dartextension type with operators070
models/category.dart, monthly_report.dartenhanced enums, CategoryLine record typedef068, 071, 076
models/entity.dart, repository/repository.dartabstract interface class, generic bounds062–063, 072–074
repository/json_file_repository.dartfiles, JSON, FormatException → StorageException106–109, 086
extensions/extension methods, generic groupBy<K>069, 073
services/ledger_service.dartasync/await, Future.wait, broadcast controller, null-aware ?? (throw ...)014, 088–091, 093–094
services/budget_monitor.dartasync*, object patterns, dot shorthands080, 096, 129
errors.dart, events/sealed hierarchies, custom exceptions082, 085
budget_app.dartcomposition, private constructor057, 066
cli/list patterns, record destructuring, StringSink, exit codes078–079, 081, 113
lib/budget_tracker.dart, pubspec.yamllibrary exports, packages, semantic versions101–105
test/, analysis_options.yamlunit and async tests, lints, strict modes114, 117–122

9. The Quality Checks

Run the three checks from the README one last time:

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

output
$ 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.00

The 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() calls balanceOf per 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 for package:args from pub.dev.
  • Features. Recurring income, CSV export, deleting wallets, dart doc API pages, and Isolate.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

dart
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');
  }
}
output
ali spends Rs 200.00 on Food

Example 2 — Money Arithmetic

dart
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'));
}
output
Rs 25.05
2505
-Rs 25.05
null

Example 3 — A Sealed Transaction

dart
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()}');
  }
}
output
2026-10-05  Transfer ali -> zayan  Rs 500.00
ali: -Rs 500.00
zayan: Rs 500.00
sara: Rs 0.00

Example 4 — A Generic Repository

dart
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'));
}
output
2
false
Wallet(sara, Sara, Card)

count and exists come from the RepositoryQueries extension.

Example 5 — A Service Rule

dart
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());
}
output
InsufficientFundsException: Wallet "ali" has only Rs 300.00, cannot take Rs 500.00.
Rs 300.00

Example 6 — Listening to Events

dart
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;
}
output
opened sara
saved t0001

Example 7 — Budget Alerts

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

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

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

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 'DeleteWalletCommand()'. Try adding a default case or cases that match 'DeleteWalletCommand()'. - non_exhaustive_switch_statement

Don'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:

output
Sara: 3 transactions, Rs 2400.00

Try it yourself before opening the solution.

Show solution
dart
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();
}
output
Sara: 3 transactions, Rs 2400.00

close() 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

  1. Trace spend ali 200 food through the project: name the file at each step, from bin/ to the JSON file.
  2. Point to one use of each in the project: sealed class, extension type, enhanced enum, record, generic class, extension method, async*.
  3. Why does bin/budget_tracker.dart contain no logic? How could a Flutter app benefit?

Application

  1. Build the binary, then run budget_tracker demo from a different, empty folder. Where is the data saved, and why?

  2. You add a new command in a later version. What is the new version number, and what goes into CHANGELOG.md?

  3. Write a test (test/cli/history_test.dart) proving that history bilal on an empty data folder returns exitFailure and prints Error: No wallet with id "bilal".

  4. Write fastBalances(LedgerService ledger) that computes every wallet's balance in one pass over the transactions, returning a Map<String, Money>. Challenge

  5. Add a delete-wallet <wallet> command end to end: help line, Command subclass, parse case, runner case, a service rule "only wallets with no transactions", a new exception, and two tests.

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

FeatureExample
sealed classTransaction, Command, LedgerEvent
extension typeMoney
enhanced enumCategory, BudgetLevel
recordCategoryLine, BudgetAlert
generic classRepository<T, Id>
extension methodIterableTools<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

output
$ /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.json

data 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

dart
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);
  });
}
output
00:00 +1: All tests passed!

Question 7 — Solution

example/fast_balances.dart:

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();
}
output
ali Rs 3100.00
sara Rs 2400.00
zayan Rs 1300.00
bilal Rs 550.00

The same numbers as wallets, in wallets + transactions steps instead of wallets × transactions.

Question 8 — Solution

app_info.dart — new line in commandHelp:

dart
  ('delete-wallet', '<wallet>', 'Delete a wallet with no transactions'),

command.dart — a parse case and a subclass:

dart
    ['delete-wallet', final wallet] => DeleteWalletCommand(wallet),
dart
final class DeleteWalletCommand extends Command {
  const DeleteWalletCommand(this.walletId);

  final String walletId;
}

errors.dart:

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:

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:

dart
      case DeleteWalletCommand(:final walletId):
        await ledger.deleteWallet(walletId);
        _out.writeln('Deleted wallet "$walletId".');

test/cli/delete_wallet_test.dart:

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>()),
    );
  });
}
output
$ 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: 1

WalletInUseException 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

  1. In which direction do the project's layers depend on each other?
  2. What does executables: in pubspec.yaml declare?
  3. Why does version: 1.0.0 matter more than 0.9.0?
  4. Which two classes listen to ledger.events?
  5. Why is Money an extension type over int instead of a double?
  6. What do exit codes 0, 1 and 64 mean in this app?
  7. What does dart compile exe give you that dart run does 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 exe builds 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:args to 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.


Video lecture

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