@docImport 으로 문서에서 import 하지 않아도 패키지를 그대로 사용할 수 있습니다.
null-aware 문법 중 ?<expression> syntax가 추가 되었습니다.
값을 연산, 접근/할당 시 null 값을 안전하게 처리합니다.
/// a의 값이 null이라면 할당하지 않습니다.
int? a;
var b = [4, 5];
var list = [1 , 2, 3, ?a, ...b]; // [1, 2, 3, 4, 5]
참고 문서: https://dart.dev/language/collections#null-aware-element
goto 유사 문법인 Label 에 break 와 continue control flow statement가 추가 되었습니다.
조건문 혹은 루프 문에서 특정 위치에서 break/continue 하기 위함입니다.
outerLoop: for (var i = 0; i < 5; i++) {
  for (var j = 0; j < 5; j++) {
    if (i + j > 5) break outerLoop;
  }
}
(…) 형태를 record literal 이라고 합니다.
final buttons = [ 
	(
    	label: "Button I", 
		icon: const Icon(Icons.upload_file), 
    	onPressed: () => print("Action -> Button I"), 
    ),
    ( 
    	label: "Button II", 
        icon: const Icon(Icons.info), 
        onPressed: () => print("Action -> Button II"),
     ),
];
위와 같이 buttons 가 있을 때,
리스트 전체인 buttons 는 List<({String label, Icon icon, void Function() onPressed})> 타입을 갖으며
단일 요소로는 ({String label, Icon icon, void Function() onPressed}) 타입을 갖습니다.
위와 같이 암묵적인 타입을 갖겠지만, 리스트 타입을 명시적으로 지정해주면 코드 수정에 더 용이할 것 같습니다.
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
  // ...
];
그러나 이렇게 보면 이전의 타입을 명시하여 작성했던 코드처럼 보입니다.
맞습니다. 결국 타입을 명시하지 않고 간결하게 작성하도록 개선된 것입니다.
즉, 타입을 명시하지 않은 경우, 아래와 같은 단점을 갖게 됩니다.
| desc | |
|---|---|
| 타입 안전성 부족 | var 또는 List<record>로 선언하지 않으면, 필드 타입이 명시되지 않아 헷갈릴 수 있음 | 
| 자동 완성 제한 | IDE에서 클래스처럼 buttons[0].label 입력 시 자동 완성이 덜 직관적일 수 있음 | 
| 재사용 어려움 | record는 재사용 구조/상속 불가 → 복잡한 로직에는 부적합 | 
| 패턴 매칭 필요 시 | 필드 이름이 변경되면 모든 코드 수정 필요 | 
참고 문서: https://dart.dev/language/records#records-as-simple-data-structures
Dart v2.9 이후 부터 런타임 오류 가능성으로 인해, 암묵적 downcast를 권장하지 않습니다.
analysis_options.yaml에 아래와 같이 추가하여 더 엄격한 prevent가 가능합니다.
analyzer:
  language:
    strict-casts: true
Before
dynamic value = "hello";
String text = value; // OK, 암묵적 downcast 허용
After
dynamic value = "hello";
String text = value; // ❌ 오류: implicit downcast from dynamic
String text2 = value as String; // ✅ 명시적 cast 필요
참고 문서: https://dart.dev/language/type-system#implicit-downcasts-from-dynamic
새롭게 추가된 trailing comma로 인해, 만일 설정해 놓은 코드의 max length를 넘어간다면
comma가 자동 생성되어 줄바꿈이 되며,
이전 코드의 length와 현재 코드의 length의 길이 합이 최대 길이보다 작으면
줄바꿈을 위해 작성했던 comma가 삭제되고, 강제로 이전 코드 라인으로 줄 변경이 이뤄집니다.
만일 comma대로 개행이 되길 원한다면 pubspec.yaml 파일에서 dart sdk의 최소 버전을
v3.6.0 이후로 지정해 줍니다.
environment:
  sdk: ">=3.6.0 <4.0.0"
Dart v3.8 이후 부터 analysis_options.yaml 파일에서 아래와 같이
comma를 보존한다는 옵션을 작성한다면,개행을 위해 명시적으로 작성했던 comma가 사라지지 않지만
comma로 구분하지 않고 이어 작성했던 코드도 comma가 생성되어 강제 개행이 됩니다.Exampleformatter: trailing_commas: preserveBefore use
runApp( ShowCaseWidget(builder: (context) { return MultiProvider( providers: getProviders(), child: const MyApp(), ); }), );After use
/// builder 라인이 comma 보존 옵션에 의해 개행됩니다. runApp( ShowCaseWidget( builder: (context) { return MultiProvider( providers: getProviders(), child: const MyApp(), ); }, ), );
analysis_options.yaml 에서 최대 코드 길이를 설정하는 옵션이 새롭게 추가 되었습니다.
formatter:
  page_width: 100 
top-level에서 동작하기 때문에 IDE의 max length와 setting.json의 설정 보다도 위에서 동작합니다.
단, 코드 길이 가이드 라인은 IDE에서 조정해야 변경됩니다.
참고 문서: https://dart.dev/tools/dart-format#configuring-formatter-page-width
이전 버전에서는 값 기반 비교였지만 이후 부터는 객체 패턴도 매치가 가능해졌습니다.
sealed class State {}
class Loading extends State {}
class Success extends State {}
class Error extends State {}
void main() {
  State state = Loading();
  switch (state) {
    case Loading():
      print('loading...');
    case Success():
      print('successful!');
    case Error():
      print('error!');
  }
}
아래와 같이 사용합니다.
ClassName(:final fieldName)
named field가 있는 객체에서 필드를 추출할 때 사용됩니다.
class Person {
  final String name;
  final int age;
  Person(this.name, this.age);
}
void main() {
  Person p = Person('h', 25);
  switch (p) {
    case Person(:final name, :final age):
      print('$name, $age');
  }
}
/// result: h, 25