DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 9: Student Information Management System

Today, we'll integrate this knowledge through a practical Student Information Management System case, experiencing the complete process from requirement analysis to code implementation, and finally to optimization and refactoring.

I. Requirement Analysis: What Are We Building?

The core requirements of a Student Information Management System are to implement CRUD operations for student information:

  1. Create: Enter student information like ID, name, and age
  2. Read: View individual student information or a list of all students
  3. Update: Modify student information such as name and age
  4. Delete: Remove information of a specified student
  5. Exit: Terminate the program

We'll implement these functions with a Dart console application, focusing on class design, collection operations, and function encapsulation.


II. Core Technical Points

Before starting coding, let's clarify the technologies we'll use:

  • Class: Define a Student class to encapsulate student information
  • List Collection: Use List to store all student data
  • Function Encapsulation: Split CRUD functions into independent functions
  • User Interaction: Communicate with users through console input/output
  • Flow Control: Implement menu logic with loops and branches

III. Code Implementation: Building the System from Scratch

1. Step 1: Define the Student Class (Data Model)

First, we need a Student class to standardize the data structure of student information, containing three core attributes: ID (unique identifier), name, and age:

class Student {
  final String id; // Student ID (immutable, unique identifier)
  String name; // Name
  int age; // Age

  // Constructor
  Student({required this.id, required this.name, required this.age});

  // Override toString method for easy information display
  @override
  String toString() {
    return "ID: $id, Name: $name, Age: $age";
  }
}
Enter fullscreen mode Exit fullscreen mode
  • id is set as final to ensure it can't be modified, preventing duplicates or accidental changes
  • Overriding toString() makes it easier to display student information clearly when printing

2. Step 2: Initialize Data and Utility Functions

Next, we need a list to store students and some utility functions (like checking if an ID exists):

// List to store all students
List<Student> students = [];

// Check if student ID already exists (to prevent duplicates)
bool isIdExists(String id) {
  return students.any((student) => student.id == id);
}
Enter fullscreen mode Exit fullscreen mode

3. Step 3: Implement Core CRUD Functions

Encapsulate each operation as an independent function to ensure single responsibility:

(1) Add Student
void addStudent() {
  print("\n===== Add Student =====");

  // Input student ID (ensure uniqueness)
  String id;
  while (true) {
    print("Please enter student ID:");
    id = stdin.readLineSync()?.trim() ?? "";
    if (id.isEmpty) {
      print("Student ID cannot be empty!");
    } else if (isIdExists(id)) {
      print("This ID already exists, please try again!");
    } else {
      break; // ID is valid, exit loop
    }
  }

  // Input name
  String name;
  while (true) {
    print("Please enter name:");
    name = stdin.readLineSync()?.trim() ?? "";
    if (name.isEmpty) {
      print("Name cannot be empty!");
    } else {
      break;
    }
  }

  // Input age (ensure it's a number)
  int age;
  while (true) {
    print("Please enter age:");
    String? ageInput = stdin.readLineSync()?.trim();
    age = int.tryParse(ageInput ?? "") ?? -1;
    if (age <= 0) {
      print("Please enter a valid age!");
    } else {
      break;
    }
  }

  // Add to list
  students.add(Student(id: id, name: name, age: age));
  print("Added successfully! Student information: ${students.last}");
}
Enter fullscreen mode Exit fullscreen mode
  • Validate input through loops (non-empty, unique ID, positive age)
  • Input validation effectively prevents dirty data
(2) Query Students

Implement two query methods: query all students and query by ID:

void queryAllStudents() {
  print("\n===== All Students =====");
  if (students.isEmpty) {
    print("No student information available!");
    return;
  }
  for (int i = 0; i < students.length; i++) {
    print("${i + 1}. ${students[i]}"); // Print with serial number
  }
}

void queryStudentById() {
  print("\n===== Query by ID =====");
  print("Please enter the student ID to query:");
  String id = stdin.readLineSync()?.trim() ?? "";

  // Find student (where returns an iterable of matching items)
  Student? student = students.where((s) => s.id == id).firstOrNull;

  if (student != null) {
    print("Query result: $student");
  } else {
    print("No student found with ID $id!");
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Use where() with firstOrNull to quickly find students
  • Empty list check improves user experience
(3) Update Student Information
void updateStudent() {
  print("\n===== Update Student =====");
  print("Please enter the ID of the student to update:");
  String id = stdin.readLineSync()?.trim() ?? "";

  Student? student = students.where((s) => s.id == id).firstOrNull;
  if (student == null) {
    print("No student found with ID $id!");
    return;
  }

  print("Current information: $student");

  // Update name (optional, press enter to skip)
  print("Please enter new name (press enter to keep current):");
  String? newName = stdin.readLineSync()?.trim();
  if (newName != null && newName.isNotEmpty) {
    student.name = newName;
  }

  // Update age (optional, press enter to skip)
  print("Please enter new age (press enter to keep current):");
  String? ageInput = stdin.readLineSync()?.trim();
  if (ageInput != null && ageInput.isNotEmpty) {
    int newAge = int.tryParse(ageInput) ?? -1;
    if (newAge > 0) {
      student.age = newAge;
    } else {
      print("Invalid age input, no changes made!");
    }
  }

  print("Update successful! Updated information: $student");
}
Enter fullscreen mode Exit fullscreen mode
  • Supports partial updates (press enter to skip), making it more flexible
  • Handles invalid inputs with error tolerance
(4) Delete Student
void deleteStudent() {
  print("\n===== Delete Student =====");
  print("Please enter the ID of the student to delete:");
  String id = stdin.readLineSync()?.trim() ?? "";

  Student? student = students.where((s) => s.id == id).firstOrNull;
  if (student == null) {
    print("No student found with ID $id!");
    return;
  }

  // Confirmation
  print("Are you sure you want to delete the following student? [y/n]");
  print(student);
  String? confirm = stdin.readLineSync()?.trim()?.toLowerCase();
  if (confirm == "y" || confirm == "yes") {
    students.remove(student);
    print("Deletion successful!");
  } else {
    print("Deletion cancelled!");
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Double confirmation prevents accidental operations, improving safety
  • Use remove() to delete elements from the list

4. Step 4: Implement Main Menu and Program Entry

Finally, we need a main loop to display the menu, receive user choices, and call corresponding functions:

import 'dart:io'; // Import input/output library

void main() {
  print("===== Student Information Management System =====");

  // Main loop (program entry)
  while (true) {
    print("\nPlease select an operation:");
    print("1. Add Student");
    print("2. View All Students");
    print("3. Query by ID");
    print("4. Update Student");
    print("5. Delete Student");
    print("0. Exit System");

    String? choice = stdin.readLineSync()?.trim();
    switch (choice) {
      case "1":
        addStudent();
        break;
      case "2":
        queryAllStudents();
        break;
      case "3":
        queryStudentById();
        break;
      case "4":
        updateStudent();
        break;
      case "5":
        deleteStudent();
        break;
      case "0":
        print("Thank you for using, goodbye!");
        return; // Exit program
      default:
        print("Invalid choice, please enter a number between 0-5!");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Use while (true) to create an infinite loop until the user chooses to exit
  • switch-case handles menu selection with clear logic

IV. Code Optimization: From "Working" to "Readable"

The code above already implements basic functionality, but we can optimize it in several ways to make it more maintainable:

1. Optimization 1: Extract Constants and Utility Classes

Extract frequently used prompt texts and menu options as constants to avoid hardcoding:

// Constant class (stores fixed text)
class Constants {
  static const String appTitle =
      "===== Student Information Management System =====";
  static const String menuTips = "\nPlease select an operation:";
  static const List<String> menuOptions = [
    "1. Add Student",
    "2. View All Students",
    "3. Query by ID",
    "4. Update Student",
    "5. Delete Student",
    "0. Exit System",
  ];
  // ... other constants
}

// Utility class (encapsulates input/output logic)
class InputUtils {
  // Safely read input (handle null and empty strings)
  static String readLine({String tip = ""}) {
    if (tip.isNotEmpty) print(tip);
    return stdin.readLineSync()?.trim() ?? "";
  }

  // Read integer (with validation)
  static int? readInt({String tip = "", int min = 0}) {
    String input = readLine(tip: tip);
    int? value = int.tryParse(input);
    return (value != null && value >= min) ? value : null;
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Optimization 2: Split Complex Functions

Split the lengthy input logic in addStudent into independent functions:

// Get valid ID from input
String getValidId() {
  while (true) {
    String id = InputUtils.readLine(tip: "Please enter student ID:");
    if (id.isEmpty) {
      print("Student ID cannot be empty!");
    } else if (isIdExists(id)) {
      print("This ID already exists, please try again!");
    } else {
      return id;
    }
  }
}

// Simplified call:
String id = getValidId();
Enter fullscreen mode Exit fullscreen mode

3. Optimization 3: Add Exception Handling

Use try-catch to catch potential exceptions (like input conversion errors):

void updateStudent() {
  try {
    // Original logic...
  } catch (e) {
    print("Operation failed: ${e.toString()}");
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Optimization 4: Improve Comments and Documentation

Add comments to classes and functions explaining their purpose, parameters, and return values:

/// Student student information entity class
/// Stores stores student ID, name, and age
class Student {
  final String id; // Student ID (unique, unmodifiable)
  String name; // Student name
  int age; // Student age (positive integer)

  /// Constructor
  /// [id]: Student ID (must be unique)
  /// [name]: Student name (non-empty)
  /// [age]: Student age (greater than 0)
  Student({required this.id, required this.name, required this.age})
    : assert(age > 0, "Age must be greater than 0"); // Assertion validation
}
Enter fullscreen mode Exit fullscreen mode

V. Extended Features: Making the System More Practical

We can extend the basic functionality with some useful features:

  1. Data Persistence: Store student information in files (persists between launches)
  2. Name-based Query: Support fuzzy search
  3. Sorting: Sort by ID or age
  4. Statistics: Calculate average age, total number of students, etc.

Take "fuzzy query by name" as an example:

void queryStudentByName() {
  print("\n===== Query by Name =====");
  String keyword = InputUtils.readLine(tip: "Please enter name keyword:");
  if (keyword.isEmpty) {
    print("Keyword cannot be empty!");
    return;
  }

  // Filter students whose names contain the keyword (case-insensitive)
  List<Student> results = students
      .where((s) => s.name.toLowerCase().contains(keyword.toLowerCase()))
      .toList();

  if (results.isEmpty) {
    print("No students found containing '$keyword'!");
  } else {
    print("Found ${results.length} results:");
    results.forEach((s) => print(s));
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)