What Are Dart Extensions? Adding Methods to Existing Classes
Examples: capitalize on String, isPrime on int
Extensions add methods, properties, and operators to existing classes without modifying source or inheriting. You can attach utilities like capitalize to String, isPrime to int, distinct to List directly on types for better readability. Generic extensions are also supported, making them very useful for widget extensions in Flutter.
Extension 기본 문법
Extension은 기존 클래스의 소스 코드를 수정하거나 상속하지 않고도 새로운 메서드, getter, setter, 연산자를 추가할 수 있는 기능입니다.
extension <ExtensionName> on <TargetType> {
// methods, getters, setters, operators
}
String 확장 예제
첫 글자 대문자 변환, 이메일 검증, 반복, 타이틀 케이스 등 문자열 유틸리티를 추가합니다.
extension StringExtension on String {
String get capitalize =>
isNotEmpty ? '${this[0].toUpperCase()}${substring(1)}' : '';
bool get isValidEmail =>
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
String repeat(int n) => List.filled(n, this).join();
String toTitleCase() {
return split(' ')
.map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1)}'
: '')
.join(' ');
}
}
// 사용 예시
String name = 'john doe';
print(name.capitalize); // John doe
print(name.toTitleCase()); // John Doe
print('hello'.repeat(3)); // hellohellohello
print('test@example.com'.isValidEmail); // true
int 확장 예제
초 단위를 시:분:초로 변환, 소수 판별, 팩토리얼 계산 등 정수에 수학 연산을 추가합니다.
extension IntExtension on int {
String toTimeString() {
int h = this ~/ 3600;
int m = (this % 3600) ~/ 60;
int s = this % 60;
return '${h.toString().padLeft(2, '0')}:'
'${m.toString().padLeft(2, '0')}:'
'${s.toString().padLeft(2, '0')}';
}
bool get isPrime {
if (this <= 1) return false;
if (this <= 3) return true;
if (this % 2 == 0 || this % 3 == 0) return false;
int i = 5;
while (i * i <= this) {
if (this % i == 0 || this % (i + 2) == 0) return false;
i += 6;
}
return true;
}
int get factorial {
if (this < 0) throw ArgumentError('음수의 팩토리얼은 정의되지 않습니다.');
if (this <= 1) return 1;
return this * (this - 1).factorial;
}
}
// 사용 예시
print(3665.toTimeString()); // 01:01:05
print(7.isPrime); // true
print(5.factorial); // 120
List<T> 제네릭 확장 예제
제네릭 타입 파라미터를 사용하여 모든 리스트 타입에 적용 가능한 유틸리티를 만듭니다.
extension ListExtension<T> on List<T> {
T? get firstOrNull => isEmpty ? null : first;
T? get lastOrNull => isEmpty ? null : last;
List<T> get distinct => toSet().toList();
List<List<T>> chunk(int size) {
return List.generate(
(length / size).ceil(),
(i) => sublist(
i * size,
(i + 1) * size > length ? length : (i + 1) * size,
),
);
}
}
// 사용 예시
List<int> numbers = [1, 2, 3, 4, 5, 1, 2];
print(numbers.distinct); // [1, 2, 3, 4, 5]
List<String> fruits = ['사과', '바나나', '오렌지', '딸기', '포도'];
print(fruits.chunk(2)); // [[사과, 바나나], [오렌지, 딸기], [포도]]
List<int> empty = [];
print(empty.firstOrNull); // null
Getter/Setter — 타입 파싱 확장
String에 안전한 타입 변환 getter를 추가하여 int, double, bool로 파싱합니다.
extension NumberParsing on String {
int? get asIntOrNull => int.tryParse(this);
double? get asDoubleOrNull => double.tryParse(this);
bool get asBool {
final lower = toLowerCase();
return lower == 'true' || lower == '1'
|| lower == 'yes' || lower == 'y';
}
}
print('42'.asIntOrNull); // 42
print('3.14'.asDoubleOrNull); // 3.14
print('abc'.asIntOrNull); // null
print('YES'.asBool); // true
정적 멤버 (Static Members)
Extension에 static 메서드를 정의할 수 있습니다. 단, 호출 시 Extension 이름을 명시해야 합니다.
extension DateTimeExtension on DateTime {
String get formattedDate =>
'$year-${month.toString().padLeft(2, '0')}-'
'${day.toString().padLeft(2, '0')}';
static DateTime fromFormattedString(String s) {
final parts = s.split('-');
if (parts.length != 3) {
throw FormatException('잘못된 날짜 형식: $s');
}
return DateTime(
int.parse(parts[0]),
int.parse(parts[1]),
int.parse(parts[2]),
);
}
static DateTime get tomorrow =>
DateTime.now().add(Duration(days: 1));
}
// 인스턴스 메서드는 직접 호출
print(DateTime.now().formattedDate); // 2023-11-15
// static 메서드는 Extension 이름으로 호출
final date = DateTimeExtension.fromFormattedString('2023-11-15');
print(DateTimeExtension.tomorrow);
제네릭 Extension — Nullable 타입 확장
T? (nullable) 타입에 Extension을 적용하면 null 안전 유틸리티를 만들 수 있습니다.
extension OptionalExtension<T> on T? {
T orDefault(T defaultValue) => this ?? defaultValue;
R? mapIf<R>(R Function(T) mapper) =>
this != null ? mapper(this as T) : null;
void ifPresent(void Function(T) action) {
if (this != null) action(this as T);
}
}
String? nullableString = null;
print(nullableString.orDefault('기본값')); // 기본값
int? nullableNumber = 42;
print(nullableNumber.orDefault(0)); // 42
String? name = '홍길동';
int? length = name.mapIf((n) => n.length); // 3
name.ifPresent((n) => print('안녕하세요, $n님!')); // 안녕하세요, 홍길동님!
이름 충돌 해결
동일 타입에 같은 이름의 메서드를 가진 Extension이 여러 개 있으면 컴파일 에러가 발생합니다. 이때 Extension 이름을 명시하여 해결합니다.
extension NumberParsing on String {
int parseInt() => int.parse(this);
}
extension StringParsing on String {
int parseInt() => int.parse(this) * 2;
}
void main() {
// '42'.parseInt(); // 컴파일 에러! 어떤 Extension인지 모호함
// Extension 이름을 명시하여 해결
print(NumberParsing('42').parseInt()); // 42
print(StringParsing('42').parseInt()); // 84
}
핵심 정리
- ✓Extension은 원본 클래스를 수정하지 않고 기능을 추가하는 강력한 도구
- ✓getter, setter, 메서드, 연산자 모두 추가 가능
- ✓제네릭 Extension으로 다양한 타입에 범용 유틸리티 구현
- ✓static 멤버는 Extension 이름으로만 호출 가능
- ⚠dynamic 타입에서는 Extension 메서드 호출 불가
- ⚠이름 충돌 시 Extension 이름을 명시하거나 import show/hide로 제어
Implementation Steps
String extension — add capitalize, isValidEmail, repeat utility methods
int extension — toTimeString(), isPrime, factorial math operations
List<T> extension — generic firstOrNull, distinct, chunk(n) safe collection utils
Conflict resolution — name extensions explicitly, control with show/hide on import
Pros
- ✓ Extend functionality without modifying original class
- ✓ Improve readability by attaching util functions directly to types
Cons
- ✗ Extension methods cannot be called on dynamic types