On this page25 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is Scope?
- 2. Top-Level Scope
- 3. Function Scope
- 4. Block Scope
- 5. Inner Scopes See Outward
- 6. Shadowing
- 7. A Shadowing Trap
- 8. What Is a Closure?
- 9. Closures Capture Variables, Not Values
- 10. makeCounter() — A Function That Remembers
- 11. Each Closure Keeps Its Own State
- 12. Closures in Loops
- 13. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 37 — Closures & Scope
| Category | 6 — Functions |
| Lecture | 037 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Beginner |
A local variable disappears when its function ends — so how can a function remember anything between calls? Scope decides where a name can be seen, and a closure is a function that keeps the variables it can see, even after their function has finished.
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 001–036:
- local and top-level variables (Lecture 030)
- parameters and return values (Lecture 031)
- arrow and anonymous functions (Lecture 034)
- storing a function in a variable, and function types such as
int Function()(Lecture 035) - functions that return other functions (Lecture 036)
Connection With Previous Lectures
In Lecture 030 we saw that a variable declared inside a function belongs to it alone, and promised to study scope in detail later. That rule creates a problem. Suppose we want a function that counts how many times it has been called:
int countVisit() {
var count = 0;
count++;
return count;
}
void main() {
print(countVisit());
print(countVisit());
print(countVisit());
}1
1
1Every call creates a brand-new count, starting at 0. A top-level variable would remember
the number, but then every function in the file could change it. We need a variable that
survives between calls and stays private. Closures give us exactly that.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what scope means and why Dart's scope is decided by the braces you write
- Identify top-level, function, and block scope
- Predict which variable a name refers to when names are shadowed
- Define a closure and explain what it captures
- Build a
makeCounter()function that remembers state between calls - Show that each closure keeps its own separate state
- Predict what a closure created inside a loop captures
- Recognize the error messages Dart produces for scope mistakes
1. What Is Scope?
The scope of a variable is the part of the program where its name can be used.
Analogy — think of a house. Something in the hallway can be seen from every room. Something inside the kitchen can be seen only in the kitchen. A person in the kitchen can still look out into the hallway, but someone in the hallway cannot see into the kitchen.
In Dart, the "rooms" are the pairs of braces { }. Scope is decided by where you write the
code, not by when it runs. This is called lexical scope.
2. Top-Level Scope
A variable declared outside every function is in top-level scope. Every function in the file can see it:
var schoolName = 'Green Valley School';
void printHeader() {
print('== $schoolName ==');
}
void main() {
printHeader();
print('Welcome to $schoolName');
}== Green Valley School ==
Welcome to Green Valley School3. Function Scope
A variable declared inside a function body — and every parameter — is in that function's scope. It exists only while the function runs:
void printTotal(int price, int quantity) {
var total = price * quantity;
print('Total: $total');
}
void main() {
printTotal(250, 4);
// print(total); // ✗ Error: Undefined name 'total'.
}Total: 1000price, quantity, and total all belong to printTotal(). main() cannot see any of them.
4. Block Scope
Every pair of braces creates its own scope — not just function bodies. A variable declared
inside an if block or a loop body disappears at the closing brace:
void main() {
var score = 70;
if (score > 50) {
var message = 'Pass';
}
print(message);
}Error: Undefined name 'message'.
print(message);
^^^^^^^To use message after the if, declare it before the block and assign it inside:
void main() {
var score = 70;
var message = 'Fail';
if (score > 50) {
message = 'Pass';
}
print(message);
}PassThe loop variable in for (var i = 0; ...) follows the same rule: it exists only inside the
loop.
5. Inner Scopes See Outward
Code can always see variables from the scopes around it — its own block, the blocks containing it, the function, and finally the top level:
var currency = 'Rs';
void main() {
var price = 500;
for (var i = 1; i <= 2; i++) {
var total = price * i;
print('$i item(s): $currency $total');
}
}1 item(s): Rs 500
2 item(s): Rs 1000Inside the loop, Dart finds total and i in the loop body, price in main(), and
currency at the top level. Seeing works outward only — never inward.
6. Shadowing
An inner scope may declare a variable with the same name as an outer one. Inside the inner scope, the new variable hides — shadows — the outer one:
var label = 'top-level';
void main() {
var label = 'main';
print(label);
if (true) {
var label = 'block';
print(label);
}
print(label);
}main
block
mainThese are three different variables. When the block ends, its label disappears and the name
refers to main()'s label again. The top-level label is never touched.
7. A Shadowing Trap
Once a block declares a name, that name means the new variable throughout the whole block — even on lines above the declaration:
void main() {
var x = 1;
if (true) {
print(x);
var x = 2;
}
}Error: Local variable 'x' can't be referenced before it is declared.
print(x);
^Dart does not quietly fall back to the outer x. Shadowing is legal, but it makes code
harder to read — prefer a different name.
8. What Is a Closure?
A closure is a function that uses a variable from a scope outside itself. The function "closes over" that variable — it takes the variable along with it:
void main() {
var name = 'Ali';
var greet = () => print('Hello, $name!');
greet();
}Hello, Ali!name is not a parameter of greet, and it is not declared inside it. greet can use it
because of lexical scope: the function was written inside main(), where name exists.
9. Closures Capture Variables, Not Values
A closure does not take a snapshot of the value. It keeps a link to the variable itself, so it always sees the variable's current value:
void main() {
var name = 'Ali';
var greet = () => print('Hello, $name!');
greet();
name = 'Sara';
greet();
}Hello, Ali!
Hello, Sara!The link works both ways — a closure can also change a captured variable, as the next step shows.
10. makeCounter() — A Function That Remembers
Now we can solve the counting problem. makeCounter() creates a local count and returns a
closure that changes it:
int Function() makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
var counter = makeCounter();
print(counter());
print(counter());
print(counter());
}1
2
3makeCounter() has finished after its first and only call, yet count lives on — the
returned closure still holds it. And nothing else in the program can reach count: it is
both remembered and private.
11. Each Closure Keeps Its Own State
Every call to makeCounter() runs its body again, creating a new count and a new
closure that captures it:
int Function() makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
var aliCounter = makeCounter();
var saraCounter = makeCounter();
print(aliCounter());
print(aliCounter());
print(saraCounter());
print(aliCounter());
}1
2
1
3Ali's counter and Sara's counter never interfere — each has its own count.
12. Closures in Loops
In a for loop, Dart creates a fresh loop variable for every iteration. A closure made
inside the loop captures that iteration's own i:
void main() {
void Function()? saved;
for (var i = 0; i < 3; i++) {
if (i == 1) {
saved = () => print('Saved i: $i');
}
}
saved!();
}Saved i: 1Compare a while loop, where the counter is one variable declared before the loop:
void main() {
void Function()? saved;
var j = 0;
while (j < 3) {
if (j == 1) {
saved = () => print('Saved j: $j');
}
j++;
}
saved!();
}Saved j: 3The closure captured the single j, and by the time it runs, the loop has moved j to 3.
Same rule as step 9: closures capture variables, not values.
13. A Real-World Example
A quiz game keeps a separate score for each player. Each tracker is a closure with its own private total:
void Function(int) makeScoreKeeper(String player) {
var total = 0;
return (int points) {
total += points;
print('$player: +$points (total $total)');
};
}
void main() {
var ali = makeScoreKeeper('Ali');
var sara = makeScoreKeeper('Sara');
ali(10);
sara(15);
ali(5);
sara(20);
}Ali: +10 (total 10)
Sara: +15 (total 15)
Ali: +5 (total 15)
Sara: +20 (total 35)Each closure captures two variables from its own call of makeScoreKeeper(): the parameter
player and the local total.
Code Examples
Example 1 — Top-Level and Local Variables
var appName = 'Quiz App';
void showUser(String user) {
var greeting = 'Hi, $user';
print('$greeting — $appName');
}
void main() {
showUser('Zayan');
}Hi, Zayan — Quiz AppExample 2 — Block Scope in a Loop
void main() {
for (var day = 1; day <= 3; day++) {
var minutes = day * 20;
print('Day $day: $minutes min');
}
}Day 1: 20 min
Day 2: 40 min
Day 3: 60 minExample 3 — Shadowing a Top-Level Variable
var city = 'Lahore';
void main() {
var city = 'Karachi';
print('Inside main: $city');
printCity();
}
void printCity() {
print('Top-level: $city');
}Inside main: Karachi
Top-level: LahoreprintCity() is written outside main(), so it sees only the top-level city.
Example 4 — A Closure Sees Changes
void main() {
var count = 5;
var show = () => print('Count is $count');
show();
count = 8;
show();
}Count is 5
Count is 8Example 5 — A Counter
int Function() makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
var next = makeCounter();
print(next());
print(next());
}1
2Example 6 — Capturing a Parameter
int Function(int) makeAdder(int amount) {
return (int value) => value + amount;
}
void main() {
var addTen = makeAdder(10);
var addHundred = makeAdder(100);
print(addTen(5));
print(addHundred(5));
}15
105A parameter is a local variable too, so a closure can capture it.
Example 7 — Student Marks
bool Function(int) makePassChecker(int passMark) {
return (int marks) => marks >= passMark;
}
void main() {
var schoolTest = makePassChecker(40);
var scholarship = makePassChecker(85);
print('Ali (72) passes test: ${schoolTest(72)}');
print('Ali (72) gets scholarship: ${scholarship(72)}');
}Ali (72) passes test: true
Ali (72) gets scholarship: falseExample 8 — Shopping Total
int Function(int) makeCart() {
var total = 0;
return (int price) {
total += price;
return total;
};
}
void main() {
var cart = makeCart();
cart(450);
cart(1200);
print('Total: ${cart(300)}');
}Total: 1950Every call adds to the same private total: 450 + 1200 + 300 = 1950.
Example 9 — Bank Balance
double Function(double) makeAccount(double opening) {
var balance = opening;
return (double amount) {
balance += amount;
return balance;
};
}
void main() {
var sara = makeAccount(1000);
print(sara(500));
print(sara(-200));
}1500.0
1300.0A positive amount is a deposit and a negative one a withdrawal. balance can only change
through the closure.
Example 10 — Highest Temperature
double Function(double) makeHighTracker() {
var highest = -100.0;
return (double reading) {
if (reading > highest) {
highest = reading;
}
return highest;
};
}
void main() {
var track = makeHighTracker();
print(track(31.5));
print(track(36.0));
print(track(29.0));
}31.5
36.0
36.0The closure remembers the highest reading so far; a lower reading leaves it unchanged.
Common Beginner Mistakes
1. Using a block variable after its block
void main() {
var marks = 80;
if (marks > 50) {
var grade = 'A';
}
// print(grade); // ✗
}Error: Undefined name 'grade'.Declare grade before the if and assign it inside.
2. Reading a name before a shadowing declaration
void main() {
var total = 100;
if (total > 50) {
// print(total); // ✗
var total = 200;
print(total);
}
}Error: Local variable 'total' can't be referenced before it is declared.Inside that block, total already means the new variable. Use a different name.
3. Shadowing by accident
var total = 0;
void addSale(int amount) {
var total = 0; // ✗ a new local variable, not the top-level one
total += amount;
}
void main() {
addSale(500);
print(total);
}0var created a new local total, so the top-level one never changed. Drop the var to use
the outer variable.
4. Calling makeCounter() every time
int Function() makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
print(makeCounter()()); // ✗ a new counter each time
print(makeCounter()());
}1
1Each makeCounter() call creates a fresh count. Create the counter once, store it, and call
the stored closure.
Best Practices
- Declare variables in the smallest scope that needs them.
- Avoid shadowing; choose a new, descriptive name instead.
- Prefer a closure's private state over a top-level variable that every function can change.
- Create a counter or tracker once, store it in a variable, then call it.
Mini Challenge
Write makeStepCounter(int step) that returns a closure. Each call adds step to a private
total and returns it. Create a counter with a step of 5 and call it three times.
Your program should print exactly:
5
10
15Try it yourself before opening the solution.
Show solution
int Function() makeStepCounter(int step) {
var total = 0;
return () {
total += step;
return total;
};
}
void main() {
var byFive = makeStepCounter(5);
print(byFive());
print(byFive());
print(byFive());
}5
10
15The closure captures both the parameter step and the local total.
Practice Questions
Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.
Beginner
-
Predict the output, then run it to check:
var color = 'red'; void main() { var color = 'blue'; print(color); } -
This program does not compile. Fix it so that it prints
Adult, without changing theprintline:void main() { var age = 20; if (age >= 18) { var status = 'Adult'; } print(status); } -
In
main(), storevar item = 'Pen';and a closureshowthat printsItem: $item. Callshow, changeitemto'Book', and callshowagain.
Application
-
Write
makeCountdown()that returns anint Function(). The closure starts from3and returns3, then2, then1on successive calls. -
Using
makeCounter()from step 10, createaliandzayancounters. Callalitwice andzayanonce, printing each result. What does the program print? -
Predict the output, then run it:
void main() { void Function()? saved; for (var i = 1; i <= 3; i++) { if (i == 2) { saved = () => print(i); } } saved!(); } -
Write
makeMultiplier(int factor)that returns anint Function(int). Use it to createtwiceandthrice, then printtwice(7)andthrice(7).
Challenge
- Write
makePiggyBank(int start)for Zayan. The closure takes an amount, adds it, and returns the new balance. Start at150, add50, then add-30, and print170. - Write
makeLimitedCounter(int max)whose closure counts1, 2, 3, …but never goes pastmax. Withmax = 3, four calls print1,2,3,3. - Write
makeAverager()whose closure takes a mark and returns the average of all marks given so far as adouble. Feed it80,90, and70and print each result.
Practice Answers
Question 1 — Solution
blueThe local color in main() shadows the top-level one.
Question 2 — Solution
void main() {
var age = 20;
var status = 'Minor';
if (age >= 18) {
status = 'Adult';
}
print(status);
}AdultQuestion 3 — Solution
void main() {
var item = 'Pen';
var show = () => print('Item: $item');
show();
item = 'Book';
show();
}Item: Pen
Item: BookQuestion 4 — Solution
int Function() makeCountdown() {
var current = 4;
return () {
current--;
return current;
};
}
void main() {
var countdown = makeCountdown();
print(countdown());
print(countdown());
print(countdown());
}3
2
1Question 5 — Solution
int Function() makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
var ali = makeCounter();
var zayan = makeCounter();
print(ali());
print(ali());
print(zayan());
}1
2
1Question 6 — Solution
2A for loop creates a fresh i for each iteration, so the closure keeps the i from the
iteration where i was 2.
Question 7 — Solution
int Function(int) makeMultiplier(int factor) {
return (int value) => value * factor;
}
void main() {
var twice = makeMultiplier(2);
var thrice = makeMultiplier(3);
print(twice(7));
print(thrice(7));
}14
21Question 8 — Solution
int Function(int) makePiggyBank(int start) {
var balance = start;
return (int amount) {
balance += amount;
return balance;
};
}
void main() {
var zayan = makePiggyBank(150);
zayan(50);
print(zayan(-30));
}170Question 9 — Solution
int Function() makeLimitedCounter(int max) {
var count = 0;
return () {
if (count < max) {
count++;
}
return count;
};
}
void main() {
var counter = makeLimitedCounter(3);
print(counter());
print(counter());
print(counter());
print(counter());
}1
2
3
3Question 10 — Solution
double Function(int) makeAverager() {
var sum = 0;
var count = 0;
return (int mark) {
sum += mark;
count++;
return sum / count;
};
}
void main() {
var average = makeAverager();
print(average(80));
print(average(90));
print(average(70));
}80.0
85.0
80.0The closure keeps two private variables. / always gives a double.
Quick Revision
- What is the scope of a variable?
- Why is Dart's scope called lexical?
- Name the three kinds of scope from this lecture.
- Can code inside a block see a variable declared in
main()? Canmain()see a variable declared inside the block? - What is shadowing, and why is it best avoided?
- What does a closure capture — a variable or a value?
- Why do two calls to
makeCounter()give two independent counters? - In a
forloop, does each iteration get its own loop variable?
Key Takeaways
- Scope is where a name can be used; Dart decides it from the braces you write (lexical scope).
- Top-level variables are visible everywhere; function variables and parameters only inside their function; block variables only inside their braces.
- Inner scopes see outward, never inward.
- A variable with the same name in an inner scope shadows the outer one — avoid it.
- A closure is a function that uses variables from an outer scope and keeps them alive.
- Closures capture variables, not values, so they see — and can change — the current value.
- Each call to a function like
makeCounter()creates new variables, so each closure has its own private state. - A
forloop gives every iteration a fresh loop variable; awhileloop's counter is one shared variable.
What's Next?
Lecture 038 — Recursion & Function Design. We have now seen functions call other functions, take functions, return functions, and remember state. One question is left: what happens when a function calls itself? Next we study recursion, and finish the category with practical rules for designing clear, well-shaped functions.