On this page24 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Plan Before You Code
- 2. User Stories
- 3. Nouns Become Models
- 4. Choosing the Right Type
- 5. Verbs Become Services
- 6. Layers and the Direction of Imports
- 7. The Folder Layout
- 8. Creating the Project
- 9. pubspec.yaml and analysis_options.yaml
- 10. The Export File and app_info.dart
- 11. The Entry Point
- 12. A Real-World Example — The Stage-132 Check
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 132 — Project Planning
| Category | 23 — Final Dart Project |
| Lecture | 132 of 140 |
| Estimated video length | 15 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
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 thelib/src/layout with an entry file pubspec.yaml, dependencies and dev dependencies,dart pub getdart create,dart run,dart analyze,dart testanddart 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 adouble - Draw the layers of an application and the direction of its imports
- Create a project with
dart createand 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 testanddart 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:
- As Ali, I want to open a wallet with an optional opening balance.
- As Sara, I want to record income with its source (allowance, scholarship).
- 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.
- As Ali, I want to transfer money to Zayan, without it counting as spending or income.
- As any student, I want a monthly limit per category (food, transport, books, rent, fun, other).
- As any student, I want a monthly report: income, spending, net, each category against its limit, and my biggest expenses.
- As any student, I want an immediate alert when an expense pushes a category near (80 %) or over its budget.
- As any student, I want one wallet's history, and wallets sorted by balance.
- As someone trying the app, I want a
democommand that fills it with sample data. - 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 stories | Model |
|---|---|
| wallet | Wallet — id, owner, name, opening balance |
| income, expense, transfer | Transaction — three kinds |
| category | Category — the six fixed categories |
| monthly limit | Budget — a category and a limit |
| money, balance, amount | Money |
| report | MonthlyReport |
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 calledfod.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, andsealed(Lecture 082) lets aswitchprove every kind is handled.Wallet,Budget→ immutable classes, withfinalfields andcopyWith.Money→ an extension type overint(Lecture 070), holding whole paisa.
Why not double for money? Look:
void main() {
print(0.1 + 0.2);
print(0.1 + 0.2 == 0.3);
print(10 + 20);
}0.30000000000000004
false
30A 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:
| Service | Owns the stories about |
|---|---|
LedgerService | wallets, income, expenses, transfers, balances, the "not enough money" rule |
BudgetService | monthly limits and reports |
BudgetMonitor | the 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:
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, MoneyThe 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:
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 138The 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:
$ 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 runThe 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:
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.6version: 0.1.0— not finished yet; Lecture 140 releases1.0.0.publish_to: none— an app, sodart pub publishcan never upload it by accident.sdk: ^3.12.0— the template writes your exact SDK version;^3.12.0accepts every 3.12. The project uses dot shorthands (Lecture 129), so it cannot go lower than 3.10.pathcame with the template and will build file paths in Lecture 135.lintsandtestare dev dependencies (Lecture 105).
analysis_options.yaml keeps the template's one line — the recommended lint set (Lecture 117):
include: package:lints/recommended.yamlLecture 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):
/// 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:
/// 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):
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');
}
}$ 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 databin/ 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.):
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:
$ 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)
void main() {
print(0.1 + 0.2);
print(0.1 + 0.2 == 0.3);
print(10 + 20);
}0.30000000000000004
false
30Example 2 — Reading the Plan
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');
}Student Budget Tracker has 9 planned commands.
First: wallets - List every wallet with its balanceExample 3 — Finding One Command
import 'package:budget_tracker/budget_tracker.dart';
void main() {
var wanted = 'spend';
for (final (name, description) in plannedCommands) {
if (name == wanted) {
print('$name: $description');
}
}
}spend: Record an expense in a categoryExample 4 — Measuring the Column Width
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');
}
}Longest name: 10 characters
wallets List every wallet with its balance
add-wallet Open a new wallet for a student
income Record money coming inThe 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
void main() {
var cents = 123450;
var rupees = cents ~/ 100;
var paisa = cents % 100;
print('Rs $rupees.${paisa.toString().padLeft(2, '0')}');
}Rs 1234.50This is the arithmetic Money will wrap in Lecture 133.
Example 6 — Category as an Enum (a Sketch)
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}');
}
}6
food
- food
- transport
- books
- rent
- fun
- otherThe real Category in Lecture 133 adds a label and more; this sketch only tests the decision.
Example 7 — Stories to Owners
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');
}
}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 -> BudgetServiceExample 8 — The 80 % Rule from Story 7
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');
}Zayan has used 84% of his food budget: warningPlain 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
// var total = 0.1 + 0.2; // ✗ 0.30000000000000004Store 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:
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
$ 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:
import 'package:budget_tracker/budget_tracker.dart';
void main() {
var total = 0;
print(appName);
}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
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');
}
}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 balanceHow it works
plannedCommandsis aconstlist and cannot be sorted, so the spread copies it.- The cascade
..sortsorts the copy and returns it;a.$1is the record's first field.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Write three more user stories (for example, "Sara wants to rename a wallet"). For each, name the class that would own it.
- In story 3, list the nouns and the verbs. Which become models, and which service owns the work?
- Why is
Categoryan enum butTransactiona sealed class?
Application
- In an example file, create a copy of
plannedCommandswith('rename', 'Rename a wallet')added at the end. Print the count, then the new line in the help-table style. - Print the first four command names as a numbered list:
1. wallets,2. add-wallet, … - Write
String formatPaisa(int paisa)and print4505,99and200000asRs 45.05,Rs 0.99andRs 2000.00.
Challenge
- Add an unused local variable to an example file and run
dart analyze. Which lint fires, and what exit code does the command return? - Store the layer rule as a
constmap from each layer to the layers it may import, and writebool mayImport(String from, String to). Print the result forservices → modelsandmodels → 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
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');
}10 commands
rename Rename a walletQuestion 5 — Solution
import 'package:budget_tracker/budget_tracker.dart';
void main() {
var number = 1;
for (final (name, _) in plannedCommands.take(4)) {
print('$number. $name');
number++;
}
}1. wallets
2. add-wallet
3. income
4. spendQuestion 6 — Solution
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));
}Rs 45.05
Rs 0.99
Rs 2000.00Question 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
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'));
}true
falsemodels may import nothing, so it can be tested alone and reused anywhere.
Quick Revision
- What three parts does a user story have?
- What do the nouns of the stories become? And the verbs?
- Why does
Moneystore whole paisa in anint? - Which way may imports point between layers?
- What does
publish_to: noneprevent? - Why does
bin/importpackage:budget_tracker/budget_tracker.dartand not asrc/file? - 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 →
intpaisa. - Layers import downward only:
bin → cli → services → repository → models. dart create -t consolegives 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.