Part 3: Introducing Structure - Functions & Classes
Welcome back to your Dart journey! In Part 2, you built a basic interactive program that asks for your pet's name and prints it. While simple, it already uses fundamental concepts like the
main
function, variables, and basic input/output.
Today, we're going to take a big leap forward. As our program grows, we need ways to organize our code, make it reusable, and represent complex "things" like our digital pet. This is where functions and classes come in.
Recap from Part 2 (Your Code So Far)
You should have something like this in your main.dart file:
// main.dart
import 'dart:io';
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
// Current status (hardcoded for now)
print('\n--- Buddy\'s Status ---');
print('Hunger: [#####-----]');
print('Happiness: [#####-----]');
print('-------------------------');
}
If you run this (dart run main.dart), it asks for a name and then prints a hardcoded status.
Step 1: Making the Status Display Reusable with a Function
Right now, if we wanted to display the pet's status in multiple places (e.g., after feeding, after playing), we'd have to copy and paste those three
print
lines every time. That's not ideal! It makes our code harder to read, and if we ever wanted to change how the status looks, we'd have to change it in many places.
This is a perfect scenario for a
function. A function is a block of code that performs a specific task and can be called (or "invoked") whenever you need that task done.
Let's create a function to display the status:
Your Task:
-
Add the function
: Below your
mainfunction (but not inside it), add the following code:
// This function will display the pet's status.
void displayPetStatus() {
print('\n--- Pet\'s Status ---');
print('Hunger: [#####-----]');
print('Happiness: [#####-----]');
print('-------------------------');
}
-
void: This means the function doesn't return any value after it's done. It just performs an action. -
displayPetStatus: This is the name of our function. Choose names that describe what the function does! -
(): Parentheses after the name indicate that it's a function. For now, it doesn't take any information (parameters) when called. -
{}: Curly braces define the "body" of the function – the code that runs when the function is called. -
Call the function: Now, replace the hardcoded print statements in your
mainfunction with a call to our new function:
// In your main function, replace the old print lines with:
displayPetStatus();
Your main.dart should now look like this:
import 'dart:io';
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
// Now we call our new function to display the status!
displayPetStatus();
}
// Our new function definition
void displayPetStatus() {
print('\n--- Pet\'s Status ---');
print('Hunger: [#####-----]');
print('Happiness: [#####-----]');
print('-------------------------');
}
Run it!
(dart run main.dart). You'll see the exact same output. But now, the status display is in its own reusable block. Imagine if we had 10 different places where we needed to show the status – one change to
displayPetStatus()
would update all of them!
Step 2: The Problem: Our Pet's Status Isn't Dynamic
The
displayPetStatus()
function is great, but it always shows "Hunger: [#####-----]". Our pet's hunger and happiness will change! How can we make this function display the actual hunger and happiness of our specific pet?
Right now, hunger and happiness aren't even variables; they're just hardcoded strings. If we added
int hunger = 5;
inside
main,
displayPetStatus()
wouldn't know about it because variables defined inside one function are not visible to others. We could pass them as parameters to the function, but there's an even better way for complex "things" like a pet.
This brings us to
Classes and Objects.
Step 3: Introducing Classes and Objects - The Digital Pet Blueprint
Think about the real world. You have a "Dog." A dog has certain characteristics (name, breed, color) and can do certain actions (bark, run, eat). Every dog is a bit different, but they all share these fundamental properties and behaviors.
In programming, a
class
is like a blueprint or a template for creating "things" (we call these "objects"). Our
DigitalPet
class will be the blueprint for all our digital pets. It will define:
-
What a pet is (its properties/data, like
name,hunger,happiness). -
What a pet can do (its actions/behaviors, like
feed,play,displayStatus).
Let's create our DigitalPet class.
Your Task:
-
Add the
DigitalPetclass : Above yourmainfunction (but below the import statement), add the following class definition:
// This is our blueprint for a Digital Pet.
class DigitalPet {
// Properties (variables) that every DigitalPet object will have.
String name;
int hunger;
int happiness;
// Constructor: A special method that runs when you create a new DigitalPet.
// It's used to set up the initial state of the object.
DigitalPet(this.name, this.hunger, this.happiness);
// 'this.name' is a shorthand for assigning the value passed to 'name'
// to the 'name' property of this DigitalPet object.
}
-
class DigitalPet { ... }: This declares a new class namedDigitalPet. -
String name; int hunger; int happiness;: These are the instance variables (or "properties") of our class. EveryDigitalPetobject will have its ownname,hunger, andhappinessvalues. -
DigitalPet(this.name, this.hunger, this.happiness);: This is the constructor. Its name is always the same as the class name. When we create aDigitalPetobject, we'll use this to give it an initial name, hunger, and happiness. -
Create a
DigitalPetobject inmain: Now, in yourmainfunction, let's create an actualDigitalPetusing our blueprint. We'll set its initial hunger and happiness.
// In your main function, replace the hardcoded status print and the displayPetStatus() call.
// Let's create our first DigitalPet object!
// We'll give it the name the user typed, and set initial hunger/happiness to 5.
DigitalPet myPet = DigitalPet(petName!, 5, 5); // '!' means we are certain petName isn't null here.
// Now, let's try to display its status using its actual properties.
print('\n--- ${myPet.name}\'s Status ---');
print('Hunger: [${myPet.hunger}]'); // Accessing the object's hunger property
print('Happiness: [${myPet.happiness}]'); // Accessing the object's happiness property
print('-------------------------');
-
DigitalPet myPet = ...: We declare a variablemyPetwhose type isDigitalPet. This variable will hold our object. -
DigitalPet(petName!, 5, 5);: This is how we call the constructor to create a newDigitalPetobject (an instance of theDigitalPetclass). We pass thepetName,5for hunger, and5for happiness. -
myPet.name,myPet.hunger,myPet.happiness: This is how you access the properties of an object using the dot (.) operator.
Your main.dart should now look like this:
import 'dart:io';
// This is our blueprint for a Digital Pet.
class DigitalPet {
// Properties (variables) that every DigitalPet object will have.
String name;
int hunger;
int happiness;
// Constructor: A special method that runs when you create a new DigitalPet.
// It's used to set up the initial state of the object.
DigitalPet(this.name, this.hunger, this.happiness);
}
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
// Let's create our first DigitalPet object!
DigitalPet myPet = DigitalPet(petName!, 5, 5);
// Now, let's try to display its status using its actual properties.
print('\n--- ${myPet.name}\'s Status ---');
print('Hunger: [${myPet.hunger}]'); // Accessing the object's hunger property
print('Happiness: [${myPet.happiness}]'); // Accessing the object's happiness property
print('-------------------------');
}
// Our old function definition (we'll modify this soon!)
void displayPetStatus() {
print('\n--- Pet\'s Status ---');
print('Hunger: [#####-----]');
print('Happiness: [#####-----]');
print('-------------------------');
}
Run it!
(dart run main.dart).
You'll see:
Welcome to Digital Pet Simulator!
What would you like to name your pet? Sparky
Hello, Sparky!
--- Sparky's Status ---
Hunger: [5]
Happiness: [5]
-------------------------
Great! Now the name is dynamic, and the numbers are coming from our
myPet
object. But the
[5]
and
[5]
don't look like a nice bar. Let's fix that.
Step 4: Moving Functions (Methods) into the Class
The
displayPetStatus()
function is really about displaying the status of a specific
DigitalPet. It makes sense to move this function inside the
DigitalPet
class. When a function belongs to a class, we call it a
method.
This is key to Object-Oriented Programming (OOP): grouping related data (properties) and behavior (methods) together into cohesive units.
Your Task:
-
Move
displayPetStatusintoDigitalPet: Cut thedisplayPetStatus()function from its current location and paste it inside theDigitalPetclass, just below the constructor. -
Rename and modify the method
:
-
Rename it from
displayPetStatusto simplydisplayStatus. Since it's insideDigitalPet, it's clear it refers to this pet's status. -
Inside the method, we can now directly access
name,hunger, andhappinessusingthis.(thoughthis.is often optional when there's no ambiguity).
-
Rename it from
// Inside your DigitalPet class:
class DigitalPet {
String name;
int hunger;
int happiness;
DigitalPet(this.name, this.hunger, this.happiness);
// Now a method of the DigitalPet class!
void displayStatus() {
print('\n--- ${this.name}\'s Status ---'); // Use this.name
print('Hunger: [${this.hunger}]'); // Use this.hunger
print('Happiness: [${this.happiness}]'); // Use this.happiness
print('-------------------------');
}
}
-
Call the method on the object
: In
main, you now calldisplayStatuson yourmyPetobject, not as a standalone function.
// In your main function:
// ...
// Now, call the method on our myPet object!
myPet.displayStatus();
Your main.dart should now look like this:
import 'dart:io';
class DigitalPet {
String name;
int hunger;
int happiness;
DigitalPet(this.name, this.hunger, this.happiness);
// This is now a method of the DigitalPet class!
void displayStatus() {
print('\n--- ${this.name}\'s Status ---');
print('Hunger: [${this.hunger}]');
print('Happiness: [${this.happiness}]');
print('-------------------------');
}
}
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
// Create our DigitalPet object
DigitalPet myPet = DigitalPet(petName!, 5, 5);
// Call the displayStatus method on our myPet object!
myPet.displayStatus();
}
Run it!
(dart run main.dart). The output will be the same, but now your code is much better organized.
displayStatus
is clearly part of what a
DigitalPet
does.
Step 5: Making the Status Bars Visual (and a private helper)
The
[5]
and
[5]
aren't as clear as the
[#####-----]
visual bar. Let's make that happen using a small helper function. We'll also introduce the concept of "private" members.
Your Task:
-
Add
MAX_STAT_VALUE: Inside yourDigitalPetclass, but before the constructor, add a constant for the maximum stat value.static constmeans it belongs to the class itself, not individual objects, and its value is fixed at compile-time.
// Inside your DigitalPet class:
class DigitalPet {
String name;
int hunger;
int happiness;
static const int MAX_STAT_VALUE = 10; // New constant!
// ... rest of your class ...
}
-
Add
_getStatusBarhelper method : Inside theDigitalPetclass, add this new method. Note the underscore_prefix, which in Dart signifies that this member is "private" to the library (meaning it's only intended for use withinmain.dartand not from other Dart files if we had them).
// Inside your DigitalPet class, after displayStatus:
// A private helper method to create a visual status bar.
// It takes a value (like hunger) and returns a string like "#####-----"
String _getStatusBar(int value) {
String bar = ''; // Start with an empty string
for (int i = 0; i < MAX_STAT_VALUE; i++) { // Loop 10 times
if (i < value) { // If the current loop number is less than our stat value
bar += '#'; // Add a '#'
} else {
bar += '-'; // Otherwise, add a '-'
}
}
return bar; // Return the finished bar string
}
This method introduces a
for
loop (more control flow!) to build the string.
It uses an
if-else
statement (more control flow!) to decide between
#
and
-.
-
Update
displayStatusto use_getStatusBar: Modify yourdisplayStatusmethod to use this new helper.
// Inside your DigitalPet class, update displayStatus:
void displayStatus() {
print('\n--- ${this.name}\'s Status ---');
print('Hunger: [${_getStatusBar(this.hunger)}]'); // Call _getStatusBar
print('Happiness: [${_getStatusBar(this.happiness)}]'); // Call _getStatusBar
print('-------------------------');
}
Your main.dart should now look like this:
import 'dart:io';
class DigitalPet {
String name;
int hunger;
int happiness;
static const int MAX_STAT_VALUE = 10; // Maximum value for stats
DigitalPet(this.name, this.hunger, this.happiness);
void displayStatus() {
print('\n--- ${this.name}\'s Status ---');
print('Hunger: [${_getStatusBar(this.hunger)}]');
print('Happiness: [${_getStatusBar(this.happiness)}]');
print('-------------------------');
}
// Private helper method (note the underscore prefix!)
String _getStatusBar(int value) {
String bar = '';
for (int i = 0; i < MAX_STAT_VALUE; i++) {
if (i < value) {
bar += '#';
} else {
bar += '-';
}
}
return bar;
}
}
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
DigitalPet myPet = DigitalPet(petName!, 5, 5);
myPet.displayStatus();
}
Run it!
(dart run main.dart).
Now you'll get the nice visual bars:
--- Sparky's Status ---
Hunger: [#####-----]
Happiness: [#####-----]
-------------------------
Step 6: Using Getters (and Arrow Functions!)
While
myPet.hunger
works, in object-oriented programming, it's often good practice to make the raw instance variables (_hunger,
_happiness) private and provide public "getters" and "setters" to control how they are accessed and modified. This is called
encapsulation.
Let's make
hunger
and
happiness
private by adding an underscore, and then create public getters. This also gives us a chance to use the concise arrow function syntax!
Your Task:
-
Make
_hungerand_happinessprivate : In yourDigitalPetclass, changehungerandhappinessto_hungerand_happiness.
class DigitalPet {
String name;
int _hunger; // Now private (indicated by leading underscore)
int _happiness; // Now private
static const int MAX_STAT_VALUE = 10;
// ...
}
- Update the constructor : The constructor needs to initialize these new private variables.
// Update constructor
DigitalPet(this.name, this._hunger, this._happiness);
- Add public getters: Add these methods to your
DigitalPetclass.
// Inside DigitalPet class, after constructor:
// Public 'getters' to access the private hunger and happiness.
// This uses Dart's concise 'arrow function' syntax (for single-expression bodies).
int get hunger => _hunger;
int get happiness => _happiness;
-
int get hunger: This declares a "getter" namedhungerthat returns anint. When you callmyPet.hunger, this code runs. -
=> _hunger;: This is the arrow function syntax. It's a shorthand forreturn _hunger;. It's very useful for short methods that just return a value or perform a single expression. -
Update
displayStatusto use getters: Even thoughdisplayStatusis inside the class, it's good practice to access the properties through their getters if they are defined.
// Inside DigitalPet class, update displayStatus:
void displayStatus() {
print('\n--- ${this.name}\'s Status ---');
print('Hunger: [${_getStatusBar(this.hunger)}]'); // Call through getter
print('Happiness: [${_getStatusBar(this.happiness)}]'); // Call through getter
print('-------------------------');
}
Note: In
_getStatusBar(this.hunger), Dart automatically uses the
get hunger
getter you just defined.
-
Update
main(No change needed!) : This is the beauty of getters. In yourmainfunction, where you callmyPet.displayStatus(), no change is needed! You're still calling it in the same way, but internally, Dart now uses your getters. This makes your class's internal implementation more flexible without affecting how you use it.
Your main.dart should now look like this:
import 'dart:io';
class DigitalPet {
String name;
int _hunger; // Now private (indicated by leading underscore)
int _happiness; // Now private
static const int MAX_STAT_VALUE = 10;
// Constructor now initializes the private variables
DigitalPet(this.name, this._hunger, this._happiness);
// Public getters to access the private hunger and happiness
int get hunger => _hunger;
int get happiness => _happiness;
void displayStatus() {
print('\n--- ${this.name}\'s Status ---');
print('Hunger: [${_getStatusBar(this.hunger)}]');
print('Happiness: [${_getStatusBar(this.happiness)}]');
print('-------------------------');
}
String _getStatusBar(int value) {
String bar = '';
for (int i = 0; i < MAX_STAT_VALUE; i++) {
if (i < value) {
bar += '#';
} else {
bar += '-';
}
}
return bar;
}
}
void main() {
print('Welcome to Digital Pet Simulator!');
stdout.write('What would you like to name your pet? ');
String? petName = stdin.readLineSync();
if (petName == null || petName.trim().isEmpty) {
petName = 'Buddy';
print('No name entered, calling your pet $petName!');
} else {
petName = petName.trim();
print('Hello, $petName!');
}
DigitalPet myPet = DigitalPet(petName!, 5, 5); // Initializing with private hunger/happiness
myPet.displayStatus();
}
Run it!
(dart run main.dart). The output remains the same, but our
DigitalPet
class is now better structured and more robust. We've taken a significant step into object-oriented principles!
Challenge for You:
Can you add a new method to the
DigitalPet
class, for example,
sayHello()? It should just print something like
"${petName} says Hi!". Remember to call it on
myPet
in your
main
function to see it in action!
What's Next?
In Part 4, we'll make our pet truly interactive! We'll use more control flow (loops and switch statements) to create a menu, and introduce asynchronous programming so our pet's actions (like eating or playing) can simulate taking time without freezing our program.