If you maintain a Flutter project long enough, the day comes when flutter analyze spits out hundreds of deprecated warnings. Everything works fine functionally, but when warnings pile up, real problems get buried. Here are the patterns from cleaning up 200+ deprecated warnings in one go.


Diagnosis: Assess the Situation with flutter analyze

flutter analyze --no-pub

Adding --no-pub skips pub package re-analysis for speed. Categorizing the output reveals that most warnings are a few repeating patterns.

info * 'withOpacity' is deprecated ... * lib/core/theme/app_theme.dart:45:22
info * 'value' is deprecated and shouldn't be used. Use 'initialValue' instead ...
info * 'activeColor' is deprecated ... Use 'activeThumbColor' instead.
warning * Unused import: 'package:go_router/go_router.dart' ...

Case 1: Color.withOpacity -> withValues(alpha:)

The most frequent deprecation. Almost all color opacity handling code is affected.

Before:

color: Colors.blue.withOpacity(0.5)
color: theme.accent.withOpacity(0.16)
border: Border.all(color: colors.border.withOpacity(0.3))

After:

color: Colors.blue.withValues(alpha: 0.5)
color: theme.accent.withValues(alpha: 0.16)
border: Border.all(color: colors.border.withValues(alpha: 0.3))

For many files, use sed to replace all at once.

# Bulk replacement across entire project (macOS)
find lib -name "*.dart" -exec sed -i '' 's/\.withOpacity(\([^)]*\))/.withValues(alpha: \1)/g' {} \;

Re-check with flutter analyze after replacement. Occasionally there are false positives if you have a custom method named withOpacity, so always review the results.


Case 2: DropdownButtonFormField.value -> initialValue

After Flutter 3.x, the initial value parameter name for DropdownButtonFormField changed.

Before:

DropdownButtonFormField<String>(
  value: _selectedCategory,
  items: ...,
  onChanged: (v) => setState(() => _selectedCategory = v),
)

After:

DropdownButtonFormField<String>(
  initialValue: _selectedCategory,
  items: ...,
  onChanged: (v) => setState(() => _selectedCategory = v),
)

There’s a subtle difference between value and initialValue. value is the controlled approach that always syncs with external state, while initialValue only sets the initial value. In most cases, switching to initialValue produces the same behavior.


Case 3: Switch.activeColor -> activeThumbColor

The Switch widget’s color properties were refined for Material 3.

Before:

Switch(
  value: _isEnabled,
  onChanged: _onToggle,
  activeColor: Colors.blue,
)

After:

Switch(
  value: _isEnabled,
  onChanged: _onToggle,
  activeThumbColor: Colors.blue,
)

If you want to change the track color too, add activeTrackColor. The old activeColor set both thumb and track to the same color, but the new API separates them.


Case 4: GoRouter location -> uri.toString()

When getting the current path in GoRouter, you need to use .uri instead of .location.

Before:

final currentPath = GoRouter.of(context)
    .routeInformationProvider
    .value
    .location;

After:

final currentPath = GoRouter.of(context)
    .routeInformationProvider
    .value
    .uri
    .toString();

location was a String, and uri is a Uri object. For path comparison or startsWith usage, using uri.path without toString() is more appropriate.


Case 5: BuildContext async gap Warning

Using context after await in an async function triggers a warning. This is because the widget might be disposed after the await, and the context reference would be stale.

Problem code:

Future<void> _onPickImage() async {
  final result = await ImagePicker().pickImage(source: ImageSource.gallery);
  if (result != null) {
    context.read<SomeBloc>().add(ImageSelected(result.path)); // warning
  }
}

Fix:

Future<void> _onPickImage() async {
  final result = await ImagePicker().pickImage(source: ImageSource.gallery);
  if (!mounted) return; // <- added
  if (result != null) {
    context.read<SomeBloc>().add(ImageSelected(result.path));
  }
}

Adding if (!mounted) return; right after await is the standard pattern.


Case 6: Miscellaneous Minor Warnings

Unnecessary string interpolation:

// Before (warning)
Text('${someVariable}')
Text('?id=${widget.id}')  // braces unnecessary

// After
Text('$someVariable')
Text('?id=$widget.id')    // but braces needed for property access
Text('?id=${widget.id}')  // braces required in this case

Unnecessary toList():

// Before
...answers.toList().map((a) => Widget())

// After (spread operator accepts Iterable directly)
...answers.map((a) => Widget())

Null-safe operator misuse:

// Using ?. on a non-nullable variable
final list = <String>[];
list?.map(...)  // warning: list is non-nullable

// Fix
list.map(...)

Summary: Priority Approach

  1. Run flutter analyze --no-pub to assess the overall situation
  2. Start with withOpacity – highest count and solvable with a single sed command
  3. Warning level (unused imports, unused variables) – manual fix per file
  4. Info level remaining deprecations – identify patterns by case then fix
  5. Re-run flutter analyze --no-pub after fixes to verify

Once cleaned up, maintaining it afterward is just a matter of checking flutter analyze results before merging PRs.