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:

// This function will display the pet's status.
void displayPetStatus() {
  print('\n--- Pet\'s Status ---');
  print('Hunger: [#####-----]');
  print('Happiness: [#####-----]');
  print('-------------------------');
}
// 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:

Let's create our DigitalPet class.

Your Task:

// 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.
}
// 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('-------------------------');

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:

// 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('-------------------------');
  }
}
// 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:

// 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 ...
}
// 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 -.

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

class DigitalPet {
  String name;
  int _hunger;   // Now private (indicated by leading underscore)
  int _happiness; // Now private

  static const int MAX_STAT_VALUE = 10;
  // ...
}
// Update constructor
DigitalPet(this.name, this._hunger, this._happiness);
// 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;
// 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.

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.

Back to Digital Pet Overview Previous: Part 2 Next: Part 4