Exercism - Grade School
This post shows you how to get Grade School exercise of Exercism.
Preparation
Before we click on our next exercise, let’s see what concepts of DART we need to consider

So we need to use the following concepts.
Classes and Private Fields
Classes define blueprints for objects. Private fields (prefixed with _) are only accessible within the same library, providing encapsulation.
class GradeSchool {
// Private field - only accessible in this file
final Map<int, Set<String>> _grades = {};
final Set<String> _allStudents = {};
// Public method
List<String> roster() {
return [];
}
}
void main() {
GradeSchool school = GradeSchool();
// Can't access _grades or _allStudents from outside
List<String> students = school.roster();
}
Maps with Sets as Values
Maps can store Sets as values, creating a structure where each key maps to a collection of unique items. This is perfect for organizing students by grade.
void main() {
// Map from grade (int) to set of student names (Set<String>)
Map<int, Set<String>> grades = {};
// Add grade 1 with students
grades[1] = {'Anna', 'Barb', 'Charlie'};
// Add grade 2 with students
grades[2] = {'Alex', 'Peter', 'Zoe'};
// Access students in grade 1
Set<String> grade1Students = grades[1]!;
print(grade1Students); // {Anna, Barb, Charlie}
// Check if grade exists
if (grades.containsKey(3)) {
print('Grade 3 exists');
}
}
Sets
Sets are collections of unique elements. They automatically prevent duplicates, making them perfect for tracking all students in the school.
void main() {
// Create set
Set<String> allStudents = {};
// Add students
allStudents.add('Jim');
allStudents.add('Anna');
allStudents.add('Jim'); // Duplicate - won't be added
print(allStudents); // {Jim, Anna}
print(allStudents.length); // 2 (only unique students)
// Check if student exists
if (allStudents.contains('Jim')) {
print('Jim is already in school');
}
}
Records (Tuples)
Records allow you to group multiple values together. They’re perfect for representing student data as (name, grade) pairs.
void main() {
// Record syntax: (name, grade)
var student = ('Jim', 2);
print(student); // (Jim, 2)
// Destructure records
var (name, grade) = student;
print(name); // Jim
print(grade); // 2
// List of records
List<(String, int)> students = [
('Jim', 2),
('Anna', 1),
('Alex', 2),
];
// Iterate with destructuring
for (var (name, grade) in students) {
print('$name is in grade $grade');
}
}
Map containsKey() Method
The containsKey() method checks if a map contains a specific key. It’s essential for checking if a grade exists before accessing it.
void main() {
Map<int, Set<String>> grades = {
1: {'Anna', 'Barb'},
2: {'Alex', 'Peter'},
};
// Check if grade exists
if (grades.containsKey(1)) {
print('Grade 1 exists');
}
// Check if grade doesn't exist
if (!grades.containsKey(3)) {
print('Grade 3 does not exist');
// Create new grade
grades[3] = {};
}
}
Map keys Property
The keys property returns an iterable of all keys in the map. You can convert it to a list and sort it.
void main() {
Map<int, Set<String>> grades = {
2: {'Alex', 'Peter'},
1: {'Anna', 'Barb'},
5: {'Jim'},
};
// Get all grade numbers
Iterable<int> gradeNumbers = grades.keys;
print(gradeNumbers); // (2, 1, 5)
// Convert to list and sort
List<int> sortedGrades = grades.keys.toList()..sort();
print(sortedGrades); // [1, 2, 5]
}
Set contains() Method
The contains() method checks if a set contains a specific element. It’s used to detect duplicate students.
void main() {
Set<String> allStudents = {'Jim', 'Anna', 'Alex'};
// Check if student exists
if (allStudents.contains('Jim')) {
print('Jim is already enrolled');
}
// Check before adding
String newStudent = 'Peter';
if (!allStudents.contains(newStudent)) {
allStudents.add(newStudent);
print('Added Peter');
} else {
print('Peter already exists');
}
}
Set add() Method
The add() method adds an element to a set. It returns true if the element was added (new), false if it already existed.
void main() {
Set<String> students = {};
// Add new student
bool added = students.add('Jim');
print(added); // true (was added)
print(students); // {Jim}
// Try to add duplicate
bool addedAgain = students.add('Jim');
print(addedAgain); // false (already exists)
print(students); // {Jim} (unchanged)
}
List toList() Method
The toList() method converts an iterable (like a Set or Map keys) to a List. This is needed for sorting.
void main() {
Set<String> students = {'Zoe', 'Alex', 'Peter'};
// Convert set to list
List<String> studentList = students.toList();
print(studentList); // [Zoe, Alex, Peter] (order may vary)
// Convert and sort
List<String> sorted = students.toList()..sort();
print(sorted); // [Alex, Peter, Zoe]
}
Cascade Operator (..)
The cascade operator (..) allows you to perform multiple operations on the same object in sequence. It’s perfect for method chaining when methods modify the object but return void.
void main() {
List<String> students = ['Zoe', 'Alex', 'Peter'];
// Without cascade: sort() returns void
// List<String> sorted = students.sort(); // Error!
// With cascade: sort() modifies list, then returns it
List<String> sorted = (students.toList()..sort());
print(sorted); // [Alex, Peter, Zoe]
// Common pattern: toList() then sort()
Set<String> studentSet = {'Zoe', 'Alex', 'Peter'};
List<String> sortedList = studentSet.toList()..sort();
print(sortedList); // [Alex, Peter, Zoe]
}
List sort() Method
The sort() method sorts a list in-place. For strings, it sorts alphabetically. It returns void, so use cascade operator to chain.
void main() {
List<String> students = ['Zoe', 'Alex', 'Peter'];
// Sort in-place
students.sort();
print(students); // [Alex, Peter, Zoe]
// With cascade operator
List<String> sorted = (students.toList()..sort());
print(sorted); // [Alex, Peter, Zoe]
}
List addAll() Method
The addAll() method adds all elements from an iterable to a list. It’s perfect for combining multiple lists.
void main() {
List<String> result = [];
// Add single elements
result.add('Anna');
result.add('Barb');
// Add multiple elements at once
List<String> grade1 = ['Anna', 'Barb', 'Charlie'];
result.addAll(grade1);
print(result); // [Anna, Barb, Anna, Barb, Charlie]
// Build result from multiple grades
List<String> grade2 = ['Alex', 'Peter'];
result.addAll(grade2);
print(result); // [Anna, Barb, Anna, Barb, Charlie, Alex, Peter]
}
For-In Loops with Destructuring
For-in loops can destructure records directly in the loop variable, making it easy to iterate over (name, grade) pairs.
void main() {
List<(String, int)> students = [
('Jim', 2),
('Anna', 1),
('Alex', 2),
];
// Iterate with destructuring
for (var (name, grade) in students) {
print('$name is in grade $grade');
// Jim is in grade 2
// Anna is in grade 1
// Alex is in grade 2
}
}
Conditional Logic
Conditional logic (if, continue) is used to check for duplicates and skip invalid operations. It’s essential for validation.
void main() {
Set<String> allStudents = {'Jim', 'Anna'};
String newStudent = 'Jim';
// Check if student already exists
if (allStudents.contains(newStudent)) {
print('Student already exists');
// Skip adding
return;
}
// Add new student
allStudents.add(newStudent);
print('Added student');
}
Null Assertion Operator (!)
The null assertion operator (!) tells Dart that a value is definitely not null. It’s used when accessing map values that we know exist.
void main() {
Map<int, Set<String>> grades = {
1: {'Anna', 'Barb'},
};
// After checking containsKey, we know the value exists
if (grades.containsKey(1)) {
Set<String> students = grades[1]!; // Safe to use !
print(students); // {Anna, Barb}
}
}
Introduction
Given students’ names along with the grade they are in, create a roster for the school.
In the end, you should be able to:
-
Add a student’s name to the roster for a grade:
- “Add Jim to grade 2.”
- “OK.”
-
Get a list of all students enrolled in a grade:
- “Which students are in grade 2?”
- “We’ve only got Jim right now.”
-
Get a sorted list of all students in all grades. Grades should be sorted as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name.
- “Who is enrolled in school right now?”
- “Let me think. We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2, and Jim in grade 5. So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe, and Jim.”
Note that all our students only have one name (it’s a small town, what do you want?), and each student cannot be added more than once to a grade or the roster. If a test attempts to add the same student more than once, your implementation should indicate that this is incorrect.
How do we solve the grade school problem?
To solve the grade school problem:
-
Data Structure: Use a
Map<int, Set<String>>to store grades and their students- Key: grade number (int)
- Value: set of student names (Set
) - ensures uniqueness per grade
-
Duplicate Tracking: Use a
Set<String>to track all students across all grades- Prevents adding the same student to multiple grades
- Quick lookup with
contains()
-
Add Students:
- Check if student already exists in
_allStudents - If exists, return
false(duplicate) - If new, add to grade’s set and
_allStudents, returntrue
- Check if student already exists in
-
Get Grade:
- Check if grade exists with
containsKey() - Return sorted list of students in that grade
- Check if grade exists with
-
Get Roster:
- Sort grades numerically
- For each grade, get students sorted alphabetically
- Combine all students in order
The key insight is using Sets to ensure uniqueness and Maps to organize students by grade, with proper sorting for the final roster.
Solution
class GradeSchool {
final Map<int, Set<String>> _grades = {};
final Set<String> _allStudents = {};
List<bool> add(List<(String, int)> students) {
final results = <bool>[];
for (final (name, grade) in students) {
// Check if student already exists anywhere in the school
if (_allStudents.contains(name)) {
results.add(false);
continue;
}
// Add student to the grade
if (!_grades.containsKey(grade)) {
_grades[grade] = {};
}
_grades[grade]!.add(name);
_allStudents.add(name);
results.add(true);
}
return results;
}
List<String> roster() {
final result = <String>[];
// Sort grades numerically
final sortedGrades = _grades.keys.toList()..sort();
// For each grade, get students sorted alphabetically
for (final grade in sortedGrades) {
final students = _grades[grade]!.toList()..sort();
result.addAll(students);
}
return result;
}
List<String> grade(int gradeNumber) {
if (!_grades.containsKey(gradeNumber)) {
return [];
}
// Return students in this grade, sorted alphabetically
final students = _grades[gradeNumber]!.toList()..sort();
return students;
}
}
Let’s break down the solution:
-
class GradeSchool- Main class:- Encapsulates the grade school roster system
- Manages students and their grades
-
final Map<int, Set<String>> _grades = {}- Grade storage:- Maps grade numbers to sets of student names
- Set ensures no duplicate students per grade
- Private field (prefixed with
_) for encapsulation
-
final Set<String> _allStudents = {}- All students tracker:- Tracks all students across all grades
- Prevents adding same student to multiple grades
- Private field for encapsulation
-
List<bool> add(List<(String, int)> students)- Add method:- Takes list of (name, grade) pairs
- Returns list of booleans indicating success/failure for each
- Handles multiple students in one call
-
final results = <bool>[]- Results list:- Stores success/failure for each student addition
true= successfully added,false= duplicate
-
for (final (name, grade) in students)- Iterate students:- Destructures each (name, grade) record
- Processes each student addition
-
if (_allStudents.contains(name))- Check duplicate:- Checks if student already exists anywhere in school
- Prevents adding same student twice
-
results.add(false)andcontinue- Handle duplicate:- Adds
falseto results (duplicate detected) - Skips to next student (doesn’t add)
- Adds
-
if (!_grades.containsKey(grade))- Check grade exists:- Checks if grade already has a set of students
- If not, creates new empty set for that grade
-
_grades[grade] = {}- Create grade set:- Initializes empty set for new grade
- Allows adding students to this grade
-
_grades[grade]!.add(name)- Add to grade:- Adds student name to grade’s set
- Uses
!because we know grade exists (checked above) - Set automatically prevents duplicates within grade
-
_allStudents.add(name)- Track student:- Adds student to global tracking set
- Ensures we can detect duplicates in future additions
-
results.add(true)- Success:- Adds
trueto results (successfully added) - Student is now in the roster
- Adds
-
return results- Return results:- Returns list of booleans for each student
- Caller can see which additions succeeded/failed
-
List<String> roster()- Get all students:- Returns sorted list of all students in all grades
- Grades sorted numerically, students alphabetically
-
final result = <String>[]- Result list:- Builds final sorted roster
- Will contain all students in order
-
final sortedGrades = _grades.keys.toList()..sort()- Sort grades:- Gets all grade numbers
- Converts to list
- Uses cascade operator to sort in-place
- Result: [1, 2, 3, 5, …] (numerically sorted)
-
for (final grade in sortedGrades)- Iterate grades:- Processes each grade in numerical order
- Ensures grades appear 1, 2, 3, etc.
-
final students = _grades[grade]!.toList()..sort()- Get sorted students:- Gets students in current grade
- Converts set to list
- Uses cascade operator to sort alphabetically
- Result: [‘Alex’, ‘Peter’, ‘Zoe’] (alphabetically sorted)
-
result.addAll(students)- Add to result:- Adds all students from current grade to result
- Maintains order: grade 1 students, then grade 2, etc.
-
return result- Return roster:- Returns complete sorted roster
- All students in order: grades sorted, students within grade sorted
-
List<String> grade(int gradeNumber)- Get grade students:- Returns sorted list of students in specific grade
- Handles non-existent grades
-
if (!_grades.containsKey(gradeNumber)) return []- Check grade exists:- If grade doesn’t exist, return empty list
- Early return for efficiency
-
final students = _grades[gradeNumber]!.toList()..sort()- Get sorted students:- Gets students in requested grade
- Converts set to list
- Sorts alphabetically using cascade operator
-
return students- Return grade list:- Returns sorted list of students in that grade
- Empty list if grade doesn’t exist
The solution efficiently manages a school roster using Maps and Sets. The Map organizes students by grade, while the Set ensures uniqueness. The add() method prevents duplicates by checking the global student set, and both roster() and grade() methods return properly sorted results. The cascade operator enables clean method chaining for sorting operations.
A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉
Visit My YouTube Channel