Old vs New Flutter
Flutter moves fast. A tutorial or Stack Overflow answer from a few years ago may use buttons that no longer exist, code without null safety, or theme properties that were renamed. Copying such code into a Flutter 3.47 project gives errors or a wall of deprecation warnings, and beginners often think they made a mistake.
This lesson gives you a short history of Flutter and Dart, a table of old APIs and their modern replacements, and the tools that update old code for you. Use it as a reference whenever you meet unfamiliar older code.
A short timeline
| When | Release | What changed for developers |
|---|---|---|
| December 2018 | Flutter 1.0 | First stable release, mobile only; Dart 2 without null safety |
| March 2021 | Flutter 2.0, Dart 2.12 | Sound null safety by default; web support stable; new button classes |
| May 2022 | Flutter 3.0 | Stable support for all six platforms |
| May 2023 | Flutter 3.10, Dart 3.0 | 100% sound null safety required; records, patterns, sealed and other class modifiers |
| November 2023 | Flutter 3.16 | Material 3 becomes the default design |
| December 2024 | Flutter 3.27 | Impeller default on Android API 29+; Row and Column get spacing; Color.withOpacity deprecated |
| November 2025 | Flutter 3.38, Dart 3.10 | Dot shorthands such as .center |
| August 2026 | Flutter 3.47, Dart 3.13 | Primary constructors; material_ui and cupertino_ui packages available as an opt-in |
Flutter now ships a stable release roughly every three months, so expect a few deprecations with each one. Deprecated APIs usually keep working for a while (with a warning) and are removed in a later release.
The null safety migration
Code written before Dart 2.12 has no ? in its types, because every type could hold null. You will recognise it by patterns like these:
- String name; // could silently be null
- int count = map['count']; // no null check
- MyWidget({Key key, this.title}) : super(key: key);
+ String? name; // nullable only when it really can be missing
+ int count = map['count'] ?? 0;
+ const MyWidget({super.key, required this.title});The old style also used @required (from package:meta) instead of the required keyword, and constructors passed key manually with : super(key: key) instead of the newer super.key super parameter (Dart 2.17).
During the move to null safety, Dart offered a tool called dart migrate, and files could opt out with a // @dart=2.9 comment. Since Dart 3.0 (May 2023) all code must be null safe: the migration tool was removed, opt-out comments no longer work, and packages that were never migrated cannot be used. If a tutorial's code has no ? anywhere, translate it using Variables & Sound Null Safety rather than trying to run it as is.
Old widgets and APIs, and their replacements
| Old (you may see) | New (use this) | Status |
|---|---|---|
FlatButton, RaisedButton, OutlineButton | TextButton, ElevatedButton, OutlinedButton (and FilledButton) | Old ones removed |
Scaffold.of(context).showSnackBar(...) | ScaffoldMessenger.of(context).showSnackBar(...) | Old one removed |
WillPopScope(onWillPop: ...) | PopScope(canPop: ..., onPopInvokedWithResult: ...) | Deprecated (Android predictive back) |
ButtonBar | OverflowBar | Deprecated |
MaterialStateProperty, MaterialState | WidgetStateProperty, WidgetState | Deprecated aliases |
color.withOpacity(0.5) | color.withValues(alpha: 0.5) | Deprecated |
textTheme.headline1, bodyText1 ... | displayLarge, bodyLarge ... (Material 3 type scale) | Old names removed |
ColorScheme.background, onBackground | surface, onSurface | Deprecated |
accentColor, primarySwatch-based themes | ColorScheme.fromSeed(...) | accentColor removed |
useMaterial3: true | Nothing; Material 3 is the default | No longer needed |
MediaQuery.of(context).textScaleFactor | MediaQuery.textScalerOf(context) | Deprecated |
MediaQuery.of(context).size | MediaQuery.sizeOf(context) | Old way works; new way rebuilds less |
Radio(groupValue: ..., onChanged: ...) | RadioGroup around Radio widgets | Deprecated |
DropdownButtonFormField(value: ...) | DropdownButtonFormField(initialValue: ...) | Deprecated |
CardTheme(...), DialogTheme(...) in ThemeData | CardThemeData(...), DialogThemeData(...) | Changed |
Here is a typical piece of 2020-era code and its modern version:
- RaisedButton(
- color: Theme.of(context).accentColor,
- onPressed: _save,
- child: Text('Save', style: Theme.of(context).textTheme.button),
- )
+ FilledButton(
+ onPressed: _save,
+ child: const Text('Save'),
+ )The new button takes its colour and text style from the Material 3 theme automatically, so most of the old styling code simply disappears. If you need to style it, use FilledButton.styleFrom(...) or a filledButtonTheme (see Material 3 Theming).
Language features older code does not use
Older code also predates many Dart features you have learned. It still compiles, but modern code is shorter and safer:
// Old style
String gradeOld(int marks) {
if (marks >= 90) {
return 'A+';
} else if (marks >= 75) {
return 'A';
}
return 'B';
}
List<Object> splitOld(String fullName) {
final parts = fullName.split(' ');
return [parts.first, parts.length];
}
// Modern style (Dart 3)
String grade(int marks) => switch (marks) {
>= 90 => 'A+',
>= 75 => 'A',
_ => 'B',
};
(String, int) split(String fullName) {
final parts = fullName.split(' ');
return (parts.first, parts.length);
}
void main() {
print('${gradeOld(92)} ${grade(92)}');
final old = splitOld('Ananya Rao');
final (first, words) = split('Ananya Rao');
print('${old[0]} ${old[1]} | $first $words');
}A+ A+ Ananya 2 | Ananya 2
The old function returns a List<Object>, so the caller loses the types and must cast; the record (String, int) keeps them. Other things to modernise when you see them: MainAxisAlignment.center can become .center (dot shorthands, Dart 3.10), a SizedBox between Row/Column children can become spacing:, and AnimatedBuilder used only to listen to a model can become ListenableBuilder.
Let the tools do the work
You rarely need to update old code by hand:
flutter analyze # lists deprecated API uses and other issues
dart fix --dry-run # shows the automatic fixes it can apply
dart fix --apply # applies them to the whole project
flutter pub outdated # shows which packages are behind
flutter upgrade # updates the Flutter SDK itselfdart fix knows many Flutter migrations (it reads fix rules that the Flutter team ships with the SDK), so it can rename deprecated APIs such as MaterialState to WidgetState across your project in one step. Review the changes with Git before committing. For bigger changes, read the breaking changes page on docs.flutter.dev, which has a migration guide for every breaking change, release by release.
Common mistakes
- Copying old tutorial code blindly. Check the date; anything before 2021 lacks null safety and uses old buttons.
- Ignoring deprecation warnings. Today's warning is next year's compile error. Fix them while they are few.
- Upgrading Flutter without running tests. Upgrade, run
dart fix --apply,flutter analyzeandflutter test, then commit. - Pinning a very old Flutter version forever. Store requirements (such as target Android API levels) eventually force you to upgrade.
Key points
- Flutter 2 (2021) brought sound null safety; Dart 3 (2023) made it mandatory and added records and patterns.
- Material 3 has been the default since Flutter 3.16; old
headline1-style text names are gone. - Old buttons and
Scaffold.of(...).showSnackBarwere removed;WillPopScope,ButtonBar,withOpacityandMaterialStateare deprecated. - Modern Dart prefers switch expressions, records,
super.key, dot shorthands andspacing. flutter analyzeanddart fix --applyfind and fix most deprecated code automatically.
Exercise
Modernise this 2020-era widget. Make it null safe, use super.key, replace the old button, theme and text style APIs, and keep the behaviour (show the title and a save button that calls onSave).
class SaveBar extends StatelessWidget { final String title; final VoidCallback onSave; SaveBar({Key key, @required this.title, this.onSave}) : super(key: key); Widget build(BuildContext context) { return ButtonBar(children: [Text(title, style: Theme.of(context).textTheme.headline6), RaisedButton(color: Theme.of(context).accentColor, onPressed: onSave, child: Text('Save'))]); } }
Show solution
import 'package:flutter/material.dart';
class SaveBar extends StatelessWidget {
const SaveBar({super.key, required this.title, this.onSave});
final String title;
final VoidCallback? onSave;
@override
Widget build(BuildContext context) {
return OverflowBar(
alignment: MainAxisAlignment.end,
spacing: 8,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
FilledButton(onPressed: onSave, child: const Text('Save')),
],
);
}
}onSave becomes VoidCallback?, because the old code did not require it; a null value simply disables the button. headline6 maps to titleLarge in the Material 3 type scale, ButtonBar becomes OverflowBar, and FilledButton takes its colour from the theme's primary colour, so accentColor is no longer needed.
Quick Quiz
1. Since which Dart version must all code be null safe?
Dart 2.12 made null safety the default, and Dart 3.0 removed the option to opt out.
2. What replaces RaisedButton?
The old button classes were replaced by TextButton, ElevatedButton and OutlinedButton, and later FilledButton.
3. Which widget replaces WillPopScope?
PopScope decides in advance whether a route can pop, which Android predictive back needs.
4. Which command applies automatic fixes for many deprecated APIs?
dart fix uses fix rules shipped with Flutter to rewrite deprecated code.
Learn Flutter & Dart with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
