You built a list, the app worked, and then a duplicate slipped in. A name got added twice, a tag showed up three times, and now you are writing a little loop that checks “have I already seen this one?” before every insert. That loop is a chore, and Dart already has the tool that does the job for you: a set. A Dart set is a collection that holds each value only once. Add the same thing twice and the second add quietly does nothing, so the duplicates never pile up in the first place. On top of that, checking whether a value is in a set is fast, much faster than scanning a list item by item. By the end of this post you will be able to create Dart sets, add and remove items, test membership, strip duplicates out of a list, and combine two sets with union, intersection, and difference.
What a Dart set actually is
A set is an unordered collection of unique values. “Unique” is the whole point: a set refuses to store the same value twice. You write one with curly braces, the same braces you use for a map, but with plain values inside instead of key-value pairs.
void main() {
Set<String> fruits = {'apple', 'pear', 'apple', 'fig'};
print(fruits); // {apple, pear, fig}
print(fruits.length); // 3
print(fruits.contains('pear')); // true
print(fruits.contains('mango')); // false
}
You put four values in, but the second ‘apple’ never made it, so the set holds three. That is the behavior you are paying for. The contains call is the other half: it answers yes or no about membership, and for a set it does that without walking the whole collection. When you find yourself asking “is this value already in here?” over and over, that question is a set-shaped question.
One thing to slow down on. Sets are unordered, so you do not index into them the way you do a list. There is no fruits[0]. If you need position, you want a list. If you need “one of each, and fast lookups,” you want a set. Dart’s default set does happen to remember the order you inserted things, which is why the printout reads back in the order you typed, but do not lean on that for anything that matters. Treat a set as a bag of unique values, not a numbered row of them.
The empty-set gotcha every beginner hits
Here is the one that trips up almost every first-semester student, and I watch it happen every term. You try to make an empty set with bare curly braces, and Dart hands you a map instead.
void main() {
var wrong = {}; // empty braces make a Map, not a Set
var right = <String>{}; // add a type and you get an empty Set
print(wrong is Map); // true
print(right is Set); // true
}
Empty braces are ambiguous, and Dart resolves the tie by calling {} a map, because maps came first in the language. The fix is to tell Dart what you mean by adding a type in front, so <String>{} is an empty set of strings and <int>{} is an empty set of ints. If your set already has values in it, like the fruits example above, Dart can figure out the type on its own and you never see the problem. It only bites on empty ones.
Adding, removing, and checking Dart sets
Once you have a set, you grow and shrink it with a small, friendly handful of methods. The interesting one is add, because it tells you whether it actually did anything.
void main() {
var seen = <String>{};
seen.add('ada');
seen.add('grace');
seen.add('ada'); // ignored, already in the set
print(seen); // {ada, grace}
print(seen.add('linus')); // true, this one was new
print(seen.add('ada')); // false, already present
seen.remove('grace');
print(seen); // {ada, linus}
seen.addAll({'edsger', 'linus'});
print(seen); // {ada, linus, edsger}
}
Read the return value of add as “did this change the set?” You get true when the value was new and false when it was already there. That single boolean often replaces the whole “have I seen this?” loop you were writing by hand: call add, and if it returns false, you know it was a repeat. remove takes a value out, addAll merges in everything from another collection (and skips anything already present, which is why the second ‘linus’ has no effect), and length tells you how many unique values you are holding.

Turning a list into a set to remove duplicates
This is the reason most people reach for a set in the first place. You have a list, it has repeats, and you want the repeats gone. Dart gives you toSet for exactly that, and toList to come back the other way.
void main() {
var scores = [90, 85, 90, 100, 85, 70];
var unique = scores.toSet();
print(unique); // {90, 85, 100, 70}
print(unique.length); // 4
var backToList = unique.toList();
print(backToList); // [90, 85, 100, 70]
}
Call toSet and every duplicate collapses to a single copy, keeping the first time each value showed up. If you need a list again, maybe because the rest of your code expects one, toList hands it back. You will see the compact one-liner scores.toSet().toList() a lot, and now you know exactly what each half is doing: toSet throws out the duplicates, toList puts the result back in list form. When you are pulling items out of a list and reshaping them, the same chaining idea shows up with map and where, which the Dart list methods post walks through.
Combining sets: union, intersection, and difference
Sets really earn their keep when you have two of them and want to compare them. Say you tracked which students showed up in week one and week two, and now you want to answer some questions about the two groups.
void main() {
var week1 = {'Ada', 'Grace', 'Linus'};
var week2 = {'Grace', 'Linus', 'Edsger'};
print(week1.union(week2)); // {Ada, Grace, Linus, Edsger}
print(week1.intersection(week2)); // {Grace, Linus}
print(week1.difference(week2)); // {Ada}
}
Three methods, three plain-English questions. union is “everyone who showed up either week,” and each person still appears only once even though Grace and Linus came both times. intersection is “who came both weeks,” which gives you Grace and Linus. difference is “who came in week one but not week two,” which is just Ada. Notice that difference is directional: week1.difference(week2) asks about people in week one only, while week2.difference(week1) would hand you Edsger instead. Whenever you catch yourself writing nested loops to figure out what two lists have in common or how they differ, that is a sign the data wanted to be sets all along.
One more thing worth knowing before you build sets of your own objects. A set decides whether two values count as duplicates by asking two questions: are they equal according to the == operator, and do they share the same hash code. For built-in types like strings and numbers that pairing just works, which is why every example here behaves the way you expect. The day you make a set of your own class and notice duplicates sneaking in anyway, that is the signal to give your class a proper == and hashCode so Dart can tell your objects apart. Until you get there, sets of strings, numbers, and the like will do exactly what you want without any of that setup.

Your next step
Here is the short version worth keeping. A Dart set holds unique values and answers membership questions fast. Build one with typed braces, and remember that empty {} is a map, so reach for <String>{} when you want an empty set. Use add, remove, and addAll to change it, and let the boolean from add tell you whether a value was new. Strip duplicates from a list with toSet, and compare two sets with union, intersection, and difference. That covers the vast majority of the times you will ever reach for a set.
The way to make it stick is to write a few. Take a list with obvious repeats, run toSet on it, and print the before and after. Make two small sets of your own and try all three comparison methods, then swap the order on difference and watch the answer change. If you want to see where sets sit next to Dart’s other collections, the Dart maps post covers key-value pairs, and the Dart functions post is handy once you start wrapping this logic into reusable pieces. For the full reference, the Dart language guide on collections lists every method. Open your editor, make a set, add the same value twice, and print it. Watch the duplicate vanish, and the idea is yours.

