distinctBy<R> method

List<E> distinctBy<R>(
  1. R keySelector(
    1. E
    ), {
  2. bool equals(
    1. R a,
    2. R b
    )?,
  3. int hashCode(
    1. R key
    )?,
  4. bool isValidKey(
    1. R key
    )?,
})

Returns a new list containing the first occurrence of each distinct element as determined by the key returned from keySelector.

Elements are considered duplicates if their keys are equal according to the provided equals function (or default equality if not specified). The order of elements in the result preserves their first occurrence.

Optional parameters allow for custom comparison logic:

  • equals: A custom equality function for comparing keys. Useful for case-insensitive comparisons or complex objects.
  • hashCode: A custom hash code function for generating hash codes for keys. Useful for optimizing performance with specific key characteristics.
  • isValidKey: A custom function to validate keys. Invalid keys are excluded entirely.

If you provide equals, you must also provide hashCode (and vice versa).

  • keySelector: Extracts the comparison key from each element. Elements with equal keys are considered duplicates.

  • equals: Optional custom equality function for comparing keys. Must be provided together with hashCode.

  • hashCode: Optional custom hash code function for keys. Must be provided together with equals and be consistent: if equals(a, b) returns true, then hashCode(a) must equal hashCode(b).

  • isValidKey: Optional predicate to filter which keys are valid. Elements whose keys fail this predicate (return false) are excluded entirely from the result. This is useful for filtering out null keys, empty strings, or other invalid values.

Performance

  • Time complexity: O(n) where n is the number of elements
  • Space complexity: O(k) where k is the number of unique valid keys
  • Uses a HashSet to track unique keys

Examples

Basic usage:

final people = [
  Person('Alice', 25),
  Person('Bob', 30),
  Person('Alice', 28), // Duplicate name
];

final uniquePeople = people.distinctBy((p) => p.name);
// Result: [Person('Alice', 25), Person('Bob', 30)]

Case-insensitive string keys:

final names = ['Alice', 'ALICE', 'Bob', 'alice'];

final uniqueNames = names.distinctBy(
  (name) => name,
  equals: (a, b) => a.toLowerCase() == b.toLowerCase(),
  hashCode: (key) => key.toLowerCase().hashCode,
);
// Result: ['Alice', 'Bob']

This method is efficient, using a native Set for standard equality and a LinkedHashSet when custom equality/hash logic is provided.

Implementation

List<E> distinctBy<R>(
  R Function(E) keySelector, {
  bool Function(R a, R b)? equals,
  int Function(R key)? hashCode,
  bool Function(R key)? isValidKey,
}) {
  if ((equals == null) != (hashCode == null)) {
    throw ArgumentError(
      'distinctBy: If you provide `equals` you must also provide `hashCode` '
      '(and vice versa). Providing only one can break hashing semantics.',
    );
  }

  final result = <E>[];

  if (equals == null && hashCode == null) {
    final seenKeys = <R>{};
    for (final element in this) {
      final key = keySelector(element);
      if (isValidKey != null && !isValidKey(key)) continue;
      if (seenKeys.add(key)) {
        result.add(element);
      }
    }
    return result;
  }

  final seenKeys = LinkedHashSet<R>(
    equals: equals,
    hashCode: hashCode,
  );
  for (final element in this) {
    final key = keySelector(element);
    if (isValidKey != null && !isValidKey(key)) continue;
    if (seenKeys.add(key)) {
      result.add(element);
    }
  }
  return result;
}