Watch the video (15 min)
On this page24 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Plan Before You Code
  5. 2. User Stories
  6. 3. Nouns Become Models
  7. 4. Choosing the Right Type
  8. 5. Verbs Become Services
  9. 6. Layers and the Direction of Imports
  10. 7. The Folder Layout
  11. 8. Creating the Project
  12. 9. pubspec.yaml and analysis_options.yaml
  13. 10. The Export File and app_info.dart
  14. 11. The Entry Point
  15. 12. A Real-World Example — The Stage-132 Check
  16. Code Examples
  17. Common Beginner Mistakes
  18. Best Practices
  19. Mini Challenge
  20. Practice Questions
  21. Practice Answers
  22. Quick Revision
  23. Key Takeaways
  24. What's Next?

Lecture 132 — Project Planning

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

Until now every program fitted in one file. A real application has dozens of classes, and starting one by typing code is how projects get lost. This lecture plans the final project — stories, types, layers and folders — and ends with an empty project that already runs.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every output and 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 067, 076–078, 082, 101–105, 113, 117 and 125:

  • enums, records and destructuring, and sealed classes
  • libraries, export, and the lib/src/ layout with an entry file
  • pubspec.yaml, dependencies and dev dependencies, dart pub get
  • dart create, dart run, dart analyze, dart test and dart format
  • lints from package:lints

Connection With Previous Lectures

Lecture 131 closed the last language topic. You now know classes, generics, records, patterns, sealed types, streams, files, JSON and testing — but each one was taught in a small program of its own. Nothing so far has shown how they fit together in one application, or where to start when that application is too big to hold in your head. The answer is not a new keyword. It is a plan.


Learning Objectives

By the end of this lecture you will be able to:

  • Describe an application with short user stories
  • Turn the nouns of the stories into models and the verbs into services
  • Choose a type for each idea: class, enum, sealed class or extension type
  • Explain why money is stored as whole paisa in an int, not a double
  • Draw the layers of an application and the direction of its imports
  • Create a project with dart create and replace the template files
  • Build the stage-132 skeleton with an export file and lib/src/app_info.dart
  • Check a project with dart format, dart analyze, dart test and dart run

1. Plan Before You Code

A project plan answers three questions before any code exists: what should the program do, which pieces does it need, and where does each piece live.

Analogy — a builder does not start laying bricks and decide on the kitchen later. An architect draws the rooms first, and every wall is placed against that drawing.

Our project is the Student Budget Tracker: a command-line app that keeps each student's wallets, records money coming in and going out, watches monthly budgets and prints reports. Everything is saved as JSON in a data/ folder.


2. User Stories

A user story is one sentence: As someone, I want something, so that a reason. Stories describe behavior without mentioning classes. These ten are the whole project:

  1. As Ali, I want to open a wallet with an optional opening balance.
  2. As Sara, I want to record income with its source (allowance, scholarship).
  3. As Zayan, I want to record an expense with a category and a note, and be stopped if my wallet doesn't hold enough money.
  4. As Ali, I want to transfer money to Zayan, without it counting as spending or income.
  5. As any student, I want a monthly limit per category (food, transport, books, rent, fun, other).
  6. As any student, I want a monthly report: income, spending, net, each category against its limit, and my biggest expenses.
  7. As any student, I want an immediate alert when an expense pushes a category near (80 %) or over its budget.
  8. As any student, I want one wallet's history, and wallets sorted by balance.
  9. As someone trying the app, I want a demo command that fills it with sample data.
  10. As the developer, I want clear error messages and exit codes, never a stack trace.

3. Nouns Become Models

Underline the nouns in the stories. Each important noun becomes a data type — a model:

Noun in the storiesModel
walletWallet — id, owner, name, opening balance
income, expense, transferTransaction — three kinds
categoryCategory — the six fixed categories
monthly limitBudget — a category and a limit
money, balance, amountMoney
reportMonthlyReport

Models only hold data and answer questions about it ("how does this transaction change Ali's balance?"). They never print, save files or read the keyboard.


4. Choosing the Right Type

Each model gets the type that fits its shape:

  • Category → enum. The list is fixed and known today, so an enhanced enum (Lecture 068) is right. A class would let anyone invent a category called fod.
  • Transaction → sealed class. Income, expense and transfer share a date and an amount but carry different extra data (a source, a category, a second wallet). That calls for subclasses, and sealed (Lecture 082) lets a switch prove every kind is handled.
  • Wallet, Budget → immutable classes, with final fields and copyWith.
  • Money → an extension type over int (Lecture 070), holding whole paisa.

Why not double for money? Look:

dart
void main() {
  print(0.1 + 0.2);
  print(0.1 + 0.2 == 0.3);
  print(10 + 20);
}
output
0.30000000000000004
false
30

A double cannot store 0.1 exactly, so small errors pile up in every total. Whole numbers are exact: Rs 0.10 + Rs 0.20 is 10 + 20 paisa. The app stores Rs 1234.50 as 123450.


5. Verbs Become Services

Now underline the verbs: open, record, transfer, set a limit, report, alert. Verbs are work, and work goes into services:

ServiceOwns the stories about
LedgerServicewallets, income, expenses, transfers, balances, the "not enough money" rule
BudgetServicemonthly limits and reports
BudgetMonitorthe immediate budget alert

Services need somewhere to keep their data. Instead of reading files directly, they talk to a repository — an object whose only job is to save and load models. Swap the repository and the same services work in memory (for tests) or on JSON files.


6. Layers and the Direction of Imports

Put the pieces in layers. Each layer may import the layers below it, never above:

output
bin/          the program: reads the command line
  ↓
cli/          turns arguments into commands, prints results
  ↓
services/     the rules: LedgerService, BudgetService, BudgetMonitor
  ↓
repository/   saving and loading
  ↓
models/       Wallet, Transaction, Category, Budget, Money

The rule protects you. A model that imported a service could never be tested or reused without that service — and models would no longer sit at the bottom.


7. The Folder Layout

The layers become folders inside lib/src/ (Lecture 125). This is where the finished project is heading — only the first file exists after today:

output
budget_tracker/
├── pubspec.yaml  analysis_options.yaml  README.md  CHANGELOG.md
├── bin/budget_tracker.dart       the entry point
├── lib/
│   ├── budget_tracker.dart       export file: the public API
│   └── src/
│       ├── app_info.dart         ← today
│       ├── models/               ← Lecture 133
│       ├── services/             ← Lecture 134
│       ├── repository/           ← Lecture 135
│       ├── extensions/  events/  ← Lectures 133, 136, 137
│       └── cli/                  ← Lecture 139
└── test/                         mirrors lib/src — Lecture 138

The roadmap: 133 models, 134 services, 135 repository and JSON files, 136 collections and generics, 137 streams and async, 138 tests, 139 analysis and refactoring, 140 the final review.


8. Creating the Project

dart create (Lecture 113) builds the starting folder. Run it where you keep your projects:

output
$ dart create -t console budget_tracker
Creating budget_tracker using template console...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  bin/budget_tracker.dart
  lib/budget_tracker.dart
  test/budget_tracker_test.dart

Running pub get...
  Resolving dependencies...
  Downloading packages...
  Changed 50 dependencies!

Created project budget_tracker in budget_tracker! In order to get started, run the following commands:

  cd budget_tracker
  dart run

The template is a placeholder: lib/budget_tracker.dart holds a calculate() function that returns 6 * 7, dart run prints Hello world: 42!, and the test checks 42. We replace every one of these files.


9. pubspec.yaml and analysis_options.yaml

Replace pubspec.yaml with:

yaml
name: budget_tracker
description: Student Budget Tracker - a command-line app for wallets, spending and budgets.
version: 0.1.0
publish_to: none

environment:
  sdk: ^3.12.0

dependencies:
  path: ^1.9.0

dev_dependencies:
  lints: ^6.0.0
  test: ^1.25.6
  • version: 0.1.0 — not finished yet; Lecture 140 releases 1.0.0.
  • publish_to: none — an app, so dart pub publish can never upload it by accident.
  • sdk: ^3.12.0 — the template writes your exact SDK version; ^3.12.0 accepts every 3.12. The project uses dot shorthands (Lecture 129), so it cannot go lower than 3.10.
  • path came with the template and will build file paths in Lecture 135. lints and test are dev dependencies (Lecture 105).

analysis_options.yaml keeps the template's one line — the recommended lint set (Lecture 117):

yaml
include: package:lints/recommended.yaml

Lecture 139 makes it stricter. After editing the pubspec, run dart pub get.


10. The Export File and app_info.dart

Create lib/src/app_info.dart. It holds the app's name, its version and the plan itself — the commands the finished app will understand, as a const list of records (Lecture 076):

dart
/// The name shown in the help text.
const appName = 'Student Budget Tracker';

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

/// The commands the finished app will understand.
const plannedCommands = [
  ('wallets', 'List every wallet with its balance'),
  ('add-wallet', 'Open a new wallet for a student'),
  ('income', 'Record money coming in'),
  ('spend', 'Record an expense in a category'),
  ('transfer', 'Move money between two wallets'),
  ('budget', 'Set a monthly limit for a category'),
  ('report', 'Show a monthly report with budget alerts'),
  ('history', 'Show the transactions of one wallet'),
  ('demo', 'Fill the app with sample data'),
];

Then replace lib/budget_tracker.dart with the export file (Lecture 102). Everything in lib/src/ is private to the package; only what this file exports is public:

dart
/// Student Budget Tracker: wallets, transactions, budgets and reports.
library;

export 'src/app_info.dart';

Every new file in Lectures 133–139 gets one more export line here.


11. The Entry Point

Replace bin/budget_tracker.dart. For now it prints the plan, destructuring each record in the for loop (Lecture 078):

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

void main(List<String> arguments) {
  print('$appName v$appVersion');
  print('Planned commands:');
  for (final (name, description) in plannedCommands) {
    print('  ${name.padRight(12)} $description');
  }
}
output
$ dart run
Student Budget Tracker v0.1.0
Planned commands:
  wallets      List every wallet with its balance
  add-wallet   Open a new wallet for a student
  income       Record money coming in
  spend        Record an expense in a category
  transfer     Move money between two wallets
  budget       Set a monthly limit for a category
  report       Show a monthly report with budget alerts
  history      Show the transactions of one wallet
  demo         Fill the app with sample data

bin/ imports the package by name, exactly as an outside user would. padRight(12) lines the descriptions up in a column.


12. A Real-World Example — The Stage-132 Check

Replace test/budget_tracker_test.dart with one smoke test, and update README.md and CHANGELOG.md (## 0.1.0 / - Project created and planned.):

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

void main() {
  test('the app has a version', () {
    expect(appVersion, '0.1.0');
  });
}

Now run the four checks you will repeat at the end of every project lecture:

output
$ dart format --output=none --set-exit-if-changed .
Formatted 4 files (0 changed) in 0.01 seconds.
$ dart analyze
Analyzing budget_tracker...
No issues found!
$ dart test -r expanded
00:00 +0: loading test/budget_tracker_test.dart
00:00 +0: test/budget_tracker_test.dart: the app has a version
00:00 +1: All tests passed!
$ dart run budget_tracker
Student Budget Tracker v0.1.0
...

Formatted, lint-free, tested and running — with no budget logic yet. That is the stage-132 skeleton, and every later lecture starts from a project in this state.


Code Examples

Create an example/ folder in the project, save each program there, and run it with dart run example/<file>.dart. Examples that import package:budget_tracker use the files you built today. The folder is scratch space — delete it when you are done.

Example 1 — Why Not double? (example/money_problem.dart)

dart
void main() {
  print(0.1 + 0.2);
  print(0.1 + 0.2 == 0.3);
  print(10 + 20);
}
output
0.30000000000000004
false
30

Example 2 — Reading the Plan

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

void main() {
  print('$appName has ${plannedCommands.length} planned commands.');
  var (firstName, firstDescription) = plannedCommands.first;
  print('First: $firstName - $firstDescription');
}
output
Student Budget Tracker has 9 planned commands.
First: wallets - List every wallet with its balance

Example 3 — Finding One Command

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

void main() {
  var wanted = 'spend';
  for (final (name, description) in plannedCommands) {
    if (name == wanted) {
      print('$name: $description');
    }
  }
}
output
spend: Record an expense in a category

Example 4 — Measuring the Column Width

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

void main() {
  var width = 0;
  for (final (name, _) in plannedCommands) {
    if (name.length > width) width = name.length;
  }
  print('Longest name: $width characters');
  for (final (name, description) in plannedCommands.take(3)) {
    print('  ${name.padRight(width + 2)}$description');
  }
}
output
Longest name: 10 characters
  wallets     List every wallet with its balance
  add-wallet  Open a new wallet for a student
  income      Record money coming in

The wildcard _ skips the description. The longest name, add-wallet, has 10 characters — which is why padRight(12) in bin/ leaves room.

Example 5 — Paisa to Rupees

dart
void main() {
  var cents = 123450;
  var rupees = cents ~/ 100;
  var paisa = cents % 100;
  print('Rs $rupees.${paisa.toString().padLeft(2, '0')}');
}
output
Rs 1234.50

This is the arithmetic Money will wrap in Lecture 133.

Example 6 — Category as an Enum (a Sketch)

dart
enum Category { food, transport, books, rent, fun, other }

void main() {
  print(Category.values.length);
  print(Category.food.name);
  for (final category in Category.values) {
    print('- ${category.name}');
  }
}
output
6
food
- food
- transport
- books
- rent
- fun
- other

The real Category in Lecture 133 adds a label and more; this sketch only tests the decision.

Example 7 — Stories to Owners

dart
const stories = [
  ('Ali', 'open a wallet', 'LedgerService'),
  ('Sara', 'record income', 'LedgerService'),
  ('Zayan', 'set a food budget', 'BudgetService'),
  ('Ali', 'see a monthly report', 'BudgetService'),
];

void main() {
  for (final (student, wish, owner) in stories) {
    print('$student wants to $wish -> $owner');
  }
}
output
Ali wants to open a wallet -> LedgerService
Sara wants to record income -> LedgerService
Zayan wants to set a food budget -> BudgetService
Ali wants to see a monthly report -> BudgetService

Example 8 — The 80 % Rule from Story 7

dart
void main() {
  var limit = 500000; // Rs 5000.00 food budget, in paisa
  var spent = 420000; // Rs 4200.00 spent so far
  var percent = spent * 100 ~/ limit;
  var level = percent >= 100
      ? 'over budget'
      : percent >= 80
      ? 'warning'
      : 'ok';
  print('Zayan has used $percent% of his food budget: $level');
}
output
Zayan has used 84% of his food budget: warning

Plain int arithmetic, so the percentage is exact. In the project this rule becomes the BudgetLevel enum.


Common Beginner Mistakes

1. Starting with code instead of stories

Writing class Wallet before the stories exist usually means rewriting it twice. Ten sentences take ten minutes and fix the scope.

2. Storing money in a double

dart
// var total = 0.1 + 0.2; // ✗ 0.30000000000000004

Store whole paisa in an int.

3. Forgetting the export line

Remove export 'src/app_info.dart'; from lib/budget_tracker.dart and dart run fails:

output
Failed to build budget_tracker:budget_tracker:
bin/budget_tracker.dart:4:11: Error: Undefined name 'appName'.
  print('$appName v$appVersion');
          ^^^^^^^

Every new file in lib/src/ needs its line in the export file.

4. Passing a command to dart run as the first word

output
$ dart run demo
Could not find package `demo` or file `demo`

The first word after dart run is the executable name. From Lecture 135 on, run commands as dart run budget_tracker demo.

5. Ignoring the analyzer

Save this as example/scratch_unused.dart — it runs, but total is never used:

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

void main() {
  var total = 0;
  print(appName);
}
output
warning - example/scratch_unused.dart:4:7 - The value of the local variable 'total' isn't used. Try removing the variable or using it. - unused_local_variable

1 issue found.

A warning makes dart analyze exit with code 2. Fix it before you go on.

6. Letting a model import a service

It compiles, but it breaks the layer rule from section 6. Arrows point down only.


Best Practices

  • Write user stories first; let the nouns and verbs suggest the classes.
  • Pick the narrowest type: enum for fixed lists, sealed classes for fixed families.
  • Store money as whole units of the smallest coin in an int.
  • Keep imports pointing down the layers.
  • Put code in lib/src/ and expose it through one export file.
  • End every working session with format, analyze, test and run all clean.
  • Record each version in CHANGELOG.md.

Mini Challenge

Write example/sorted_help.dart that prints the planned commands in alphabetical order, without changing plannedCommands.

Try it yourself before opening the solution.

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

void main() {
  var sorted = [...plannedCommands]..sort((a, b) => a.$1.compareTo(b.$1));
  for (final (name, description) in sorted) {
    print('${name.padRight(12)} $description');
  }
}
output
add-wallet   Open a new wallet for a student
budget       Set a monthly limit for a category
demo         Fill the app with sample data
history      Show the transactions of one wallet
income       Record money coming in
report       Show a monthly report with budget alerts
spend        Record an expense in a category
transfer     Move money between two wallets
wallets      List every wallet with its balance

How it works

  1. plannedCommands is a const list and cannot be sorted, so the spread copies it.
  2. The cascade ..sort sorts the copy and returns it; a.$1 is the record's first field.

Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Write three more user stories (for example, "Sara wants to rename a wallet"). For each, name the class that would own it.
  2. In story 3, list the nouns and the verbs. Which become models, and which service owns the work?
  3. Why is Category an enum but Transaction a sealed class?

Application

  1. In an example file, create a copy of plannedCommands with ('rename', 'Rename a wallet') added at the end. Print the count, then the new line in the help-table style.
  2. Print the first four command names as a numbered list: 1. wallets, 2. add-wallet, …
  3. Write String formatPaisa(int paisa) and print 4505, 99 and 200000 as Rs 45.05, Rs 0.99 and Rs 2000.00.

Challenge

  1. Add an unused local variable to an example file and run dart analyze. Which lint fires, and what exit code does the command return?
  2. Store the layer rule as a const map from each layer to the layers it may import, and write bool mayImport(String from, String to). Print the result for services → models and models → services.

Practice Answers

Question 1 — Solution

Sample answers: As Sara, I want to rename a wallet → LedgerService (it owns wallets). As Zayan, I want to remove a budget → BudgetService. As Ali, I want to export a report to a file → BudgetService builds the report; the CLI writes it.

Question 2 — Solution

Nouns: expense, category, note, wallet, money → models Transaction (an expense), Category, Wallet, Money. Verbs: record, stop → LedgerService, which checks the balance before saving.

Question 3 — Solution

The categories are a fixed list of plain values, so an enum fits. Transactions also come in a fixed set, but each kind carries different data (source, category, receiving wallet), so they need subclasses; sealed keeps the set closed so a switch is checked to cover them all.

Question 4 — Solution

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

void main() {
  var commands = [...plannedCommands, ('rename', 'Rename a wallet')];
  print('${commands.length} commands');
  var (name, description) = commands.last;
  print('  ${name.padRight(12)} $description');
}
output
10 commands
  rename       Rename a wallet

Question 5 — Solution

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

void main() {
  var number = 1;
  for (final (name, _) in plannedCommands.take(4)) {
    print('$number. $name');
    number++;
  }
}
output
1. wallets
2. add-wallet
3. income
4. spend

Question 6 — Solution

dart
String formatPaisa(int paisa) {
  var rupees = paisa ~/ 100;
  var rest = (paisa % 100).toString().padLeft(2, '0');
  return 'Rs $rupees.$rest';
}

void main() {
  print(formatPaisa(4505));
  print(formatPaisa(99));
  print(formatPaisa(200000));
}
output
Rs 45.05
Rs 0.99
Rs 2000.00

Question 7 — Solution

unused_local_variable, reported as a warning; dart analyze exits with code 2. See Common Mistake 5 for the exact output.

Question 8 — Solution

dart
const allowed = {
  'bin': ['cli'],
  'cli': ['services'],
  'services': ['repository', 'models'],
  'repository': ['models'],
  'models': <String>[],
};

bool mayImport(String from, String to) => allowed[from]!.contains(to);

void main() {
  print(mayImport('services', 'models'));
  print(mayImport('models', 'services'));
}
output
true
false

models may import nothing, so it can be tested alone and reused anywhere.


Quick Revision

  1. What three parts does a user story have?
  2. What do the nouns of the stories become? And the verbs?
  3. Why does Money store whole paisa in an int?
  4. Which way may imports point between layers?
  5. What does publish_to: none prevent?
  6. Why does bin/ import package:budget_tracker/budget_tracker.dart and not a src/ file?
  7. Which four commands check a stage?

Key Takeaways

  • Plan first: user stories fix what the program must do.
  • Nouns become models; verbs become services; storage goes behind a repository.
  • Fixed list → enum; fixed family with different data → sealed class; money → int paisa.
  • Layers import downward only: bin → cli → services → repository → models.
  • dart create -t console gives a starting point; every template file gets replaced.
  • lib/src/ holds the code; one export file is the public API.
  • The stage-132 skeleton is formatted, lint-free, tested and running — before any logic exists.

What's Next?

Lecture 133 — Project Models. The plan names six nouns; next they become real Dart types. We will build Money as an extension type, Category as an enhanced enum, immutable Wallet and Budget classes with JSON support, and the sealed Transaction family whose exhaustive switch calculates every balance.


Video lecture

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