Skip to content

Project & Tooling

flutter create scaffolds a standard shape every Flutter project shares:

my_app/
lib/ # your Dart source — main.dart is the entry point
test/ # unit/widget tests, mirrors lib/'s structure
android/ ios/ web/ # per-platform native project (generated, rarely hand-edited)
macos/ windows/ linux/ # desktop targets, same idea
pubspec.yaml # the manifest — dependencies, assets, SDK constraint
analysis_options.yaml # linter/analyzer configuration

lib/ and test/ are where you live day to day; the platform folders exist so each target has a real native project underneath (Gradle for Android, an Xcode project for iOS/macOS, and so on) — you only reach into them for platform-specific config (permissions, native plugin setup, signing).

Every Flutter project is described by pubspec.yaml — dependencies, dev-only dependencies, and the assets bundled into the app.

name: my_app
environment:
sdk: ^3.13.0 # the Dart SDK that ships with Flutter 3.47
dependencies:
flutter:
sdk: flutter
go_router: ^18.0.0 # runtime dependency
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0 # only used during development/tests
flutter:
uses-material-design: true
assets:
- assets/images/ # a whole folder
- assets/config.json # a single file

The split matters: dependencies ship inside your app; dev_dependencies (test frameworks, linters, code generators) do not. Assets listed here are the only ones bundled — a missing entry is the usual cause of “asset not found.”

The flutter command is the entry point to everything:

Terminal window
flutter create my_app # scaffold a new project
flutter pub get # resolve and download dependencies
flutter run # run on a connected device/emulator
flutter run --release # run in release mode
flutter build apk # produce a release build
flutter doctor # diagnose your local toolchain
flutter analyze # static analysis / lints

flutter doctor is the first thing to run when anything is off — real output from this exact machine (Flutter 3.47.5):

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.47.5, on macOS 26.6.2 25G83 darwin-arm64, locale en-TH)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 26.2)
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
[✓] Network resources
• No issues found!

It checks the SDK, every platform’s device toolchain, and IDE plugins, and tells you exactly what’s missing — a red [✗] line names the specific thing to fix (an unaccepted Android license, a missing Xcode component, …) rather than a generic failure.

FVM: pinning a Flutter SDK version per project

Section titled “FVM: pinning a Flutter SDK version per project”

FVM (Flutter Version Management, fvm.app) is the community-standard tool for exactly the problem this course itself has to manage: pinning a specific Flutter SDK version per project (.fvmrc / .fvm/fvm_config.json) instead of whatever version happens to be on PATH. It is not part of the Flutter SDK itself — it’s a separate CLI that manages multiple cached Flutter SDK installs and symlinks the right one into each project — but it’s the standard answer to “why does this build differently on my machine vs. CI” when the real cause is two different Flutter versions.

analysis_options.yaml: strictness that catches real bugs

Section titled “analysis_options.yaml: strictness that catches real bugs”

analysis_options.yaml configures the analyzer beyond the base lint set. Three flags — strict-casts, strict-inference, and strict-raw-types — are not part of any lint package; they’re analyzer language options, off by default, that catch a specific class of bug: code that only “works” because of an implicit dynamic.

analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

This course’s own probe runs with exactly this configuration — every diagnostic below is real, captured by running flutter analyze against small demo files with these three flags on.

// @expect-error strict-casts: real diagnostic captured with strict-casts:
// true enabled, from a scratch file inside this course's own probe
dynamic getValue() => 42;
int useStrictCasts() {
int value = getValue(); // implicit dynamic -> int cast
return value;
}
error • A value of type 'dynamic' can't be assigned to a variable of type 'int'. Try changing the type of the variable, or casting the right-hand type to 'int' • invalid_assignment

Without strict-casts, this implicit narrowing from dynamic is silently allowed — a real source of “it worked in this one code path but crashed with a type error somewhere else at runtime” bugs, now caught at analyze time instead.

// @expect-error strict-raw-types: real diagnostic captured with
// strict-raw-types: true enabled
List useStrictRawTypes() {
final List list = []; // raw type, no type argument
return list;
}
warning • The generic type 'List<dynamic>' should have explicit type arguments but doesn't. Use explicit type arguments for 'List' • strict_raw_type
// @expect-error strict-inference: real diagnostic captured with
// strict-inference: true enabled
List computeList() {
final result = []; // inferred as List<dynamic> -- strict-inference flags this
return result;
}
warning • The type argument(s) of 'List' can't be inferred. Use explicit type argument(s) for 'List' • inference_failure_on_collection_literal

The fix for all three is the same: be explicit.

lib/strict_mode_fixed.dart
int getIntValue() => 42; // no dynamic return type to begin with
int useStrictCastsFixed() {
final int value = getIntValue(); // no implicit cast needed
return value;
}
List<String> useStrictRawTypesFixed() {
final List<String> list = <String>[]; // explicit type argument
return list;
}
List<int> computeListFixed() {
final result = <int>[]; // explicit type argument
return result;
}

This fence analyzes clean against the exact same strict settings — running flutter analyze over it reports zero issues.

flutter_lints vs. very_good_analysis, proven with a real gap

Section titled “flutter_lints vs. very_good_analysis, proven with a real gap”

Both are lint-rule packages you include: from analysis_options.yaml; they are not equivalent, and the difference isn’t always where you’d guess. Checking both packages’ real, installed rule sets on this machine (flutter_lints resolves to 6.0.0, very_good_analysis resolves to 11.0.0 via flutter pub add --dry-run against current pub.dev):

  • flutter_lints 6.0.0 (include: package:flutter_lints/flutter.yaml) layers a small, Flutter-specific set on top of package:lints/recommended.yaml: avoid_print, avoid_unnecessary_containers, avoid_web_libraries_in_flutter, no_logic_in_create_state, prefer_const_constructors_in_immutables, sized_box_for_whitespace, sort_child_properties_last, use_build_context_synchronously, use_full_hex_values_for_flutter_colors, use_key_in_widget_constructors — a deliberately small, low-friction default.
  • very_good_analysis 11.0.0 is much larger (237 lines of rules) and notably does include the general prefer_const_constructors — reading flutter_lints’ own lib/flutter.yaml line by line shows the reverse is true: it ships only the narrower prefer_const_constructors_in_immutables (which only fires for a constructor call inside a class marked @immutable), not the general rule used throughout the rebuilds-and-performance lesson’s const-skip proof.

The practical takeaway: flutter_lints alone will not flag a missing const on an ordinary StatelessWidget call site — if that specific win matters to your team as an enforced rule (not just a habit), either add prefer_const_constructors explicitly under this project’s own linter: rules: section, or adopt very_good_analysis, which ships it by default alongside a much stricter overall baseline.

dart fix applies the analyzer’s own registered auto-fixes. --dry-run shows what it would change without touching anything:

// @expect-error use_full_hex_values_for_flutter_colors: real diagnostic +
// dart fix --dry-run output captured from a scratch file inside this
// course's own probe (see the info-level output quoted below)
import 'package:flutter/material.dart';
const Color brandColor = Color(0xFFFFF); // triggers use_full_hex_values_for_flutter_colors

flutter analyze on this file reports the real diagnostic:

info • Instances of 'Color' should be created using an 8-digit hexadecimal integer (such as '0xFFFFFFFF') • use_full_hex_values_for_flutter_colors

Running dart fix --dry-run against a file with exactly this line (captured from this course’s own probe):

Computing fixes in scratch_fix (dry run)...
1 proposed fix in 1 file.
fixable_demo.dart
use_full_hex_values_for_flutter_colors - 1 fix
To fix an individual diagnostic, run one of:
dart fix --apply --code=use_full_hex_values_for_flutter_colors lib/scratch_fix
To fix all diagnostics, run:
dart fix --apply lib/scratch_fix

Not every lint is auto-fixable, though — proven while building this exact example: avoid_unnecessary_containers (a real, live diagnostic from flutter analyze) reported zero proposed fixes from dart fix --dry-run on the same file, because removing a Container while correctly re-parenting its child is a structural edit the fix registry doesn’t attempt automatically. dart fix is genuinely useful, but “the analyzer flagged it” and “dart fix can fix it” are two different, independently-provable facts.

dart:io vs. web — proven with a real build (correcting a common assumption)

Section titled “dart:io vs. web — proven with a real build (correcting a common assumption)”

The common assumption is that flutter build web fails outright the moment you import 'dart:io' and touch File. Running it for real, in a scratch Flutter app with a direct dart:io File call wired into main() (this course’s probe never gets a web-incompatible import, per the shared brief — this was run in scratch, never the probe):

// scratch/lib/dart_io_direct.dart -- NOT run in the shared probe, per the brief
import 'dart:io';
String readConfigPath() {
final file = File('config.json');
return file.path;
}
bool isIOSPlatform() => Platform.isIOS;
Future<String> readConfigContents() async {
final file = File('config.json');
return file.readAsString();
}
$ flutter build web
Compiling lib/main.dart for the Web...
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7800 bytes (99.5% reduction).
Compiling lib/main.dart for the Web... 13.6s
✓ Built build/web

It succeeds — with Platform.isIOS and File.readAsString() both compiled in and called from main(). Same result with flutter build web --wasm. The reason, proven by reading this exact installed SDK’s own web patch source (~/flutter/bin/cache/dart-sdk/lib/_internal/js_runtime/lib/io_patch.dart): dart:io’s classes exist as real, type-checkable stubs on the web compiler — every native operation just throws at runtime instead of failing to compile:

~/flutter/bin/cache/dart-sdk/lib/_internal/js_runtime/lib/io_patch.dart
@patch
class _File {
@patch
static _exists(_Namespace namespace, Uint8List path) {
throw UnsupportedError("File._exists");
}
@patch
static _create(_Namespace namespace, Uint8List path, bool exclusive) {
throw UnsupportedError("File._create");
}
// ...every other native File/Directory/FileStat operation follows the same pattern
}

This is the actual danger, and it’s worse than a build failure: the bug ships silently through every gate (flutter analyze, flutter build web, flutter build web --wasm — none of them catch it), and only surfaces as a real UnsupportedError the first time a user’s browser actually executes that code path in production. This course’s environment has no browser to run and capture that runtime crash directly — cited from source above, honestly, rather than asserted without evidence.

kIsWeb (from package:flutter/foundation.dart) is the runtime check most code reaches for — if (!kIsWeb) { file.readAsStringSync(); } — and it does work as a runtime guard. But it does not change what’s compiled: the dart:io-importing code is still part of the web bundle either way (proven above — the build never failed even for the unguarded version), so a kIsWeb check alone gives you no compile-time signal if you forget it somewhere. The more robust fix is a conditional import/export, which swaps the entire implementation per platform instead of branching at runtime inside one:

lib/platform_stub.dart
String readConfigValue() =>
throw UnsupportedError('No platform implementation selected');
lib/platform_io_impl.dart
import 'dart:io';
String readConfigValue() => Platform.environment['CONFIG'] ?? 'io-default';
lib/platform_web_impl.dart
String readConfigValue() => 'web-default'; // no dart:io reference at all
lib/platform_config.dart
export 'platform_stub.dart'
if (dart.library.io) 'platform_io_impl.dart'
if (dart.library.js_interop) 'platform_web_impl.dart';
test/platform_config_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:probe/lessons/tooling-and-production__project-and-tooling/platform_config.dart';
void main() {
test(
'the conditional export selects the dart:io implementation under flutter test (native VM)',
() {
expect(readConfigValue(), 'io-default');
});
}
00:00 +0: the conditional export selects the dart:io implementation under flutter test (native VM)
00:00 +1: All tests passed!

dart.library.io is true under flutter test (a native VM process), so this test proves the io_impl branch is genuinely the one selected — not just that the file compiles. On web, the same export line resolves dart.library.js_interop instead and swaps in platform_web_impl.dart, whose file never even has a dart:io import to compile in the first place. universal_io (a pub.dev package providing a dart:io-shaped API that’s actually web-safe for the HTTP-client subset) is the ready-made version of this pattern for the common case of wanting HttpClient-shaped code that also runs on web.

DevTools is the browser-based suite for inspecting a running app: the widget inspector (tap a widget, see its tree and constraints), the performance/timeline view (find jank, covered with its own real workflow in the rebuilds-and-performance lesson), the memory view, and the network view. It’s how you see the trees and pipeline the earlier modules described — this course’s environment has no device to attach it to, so every claim about it in this course is docs-cited, never a screenshot.

The two refresh commands are not the same, and knowing the difference saves real confusion:

  • Hot reload (r) injects changed source into the running app and rebuilds the widget tree — State is preserved. It’s sub-second and is the reason Flutter development feels fast.
  • Hot restart (R) throws away app state and restarts from main() — slower, but a clean slate.

Hot reload has limits. It re-runs build, but it does not re-run initState or a changed main(), and it can’t apply certain changes (new const evaluations, changes to enum/class shape, global variable initializers). When a change doesn’t seem to take, the answer is usually hot restart.

AI-generated project setup and config code reliably assumes a lint/analyzer baseline stricter than what’s actually configured, or reaches for dart:io in code that’s meant to be platform-shared without checking.

// AI-generated: a shared "read local settings file" helper,
// used from both a mobile screen and (the AI assumed) a web build too
import 'dart:io';
class SettingsRepository {
Future<String> loadRawSettings() {
return File('settings.json').readAsString();
}
}

Before trusting generated project/tooling code like this, check for:

  • A dart:io import in any file that’s reachable from a web build target — proven above, this compiles fine and ships silently; only a conditional import/export (or universal_io) actually prevents it from being part of the web bundle at all.
  • Assuming prefer_const_constructors (or any specific lint) is active just because flutter_lints is included — proven above, flutter_lints 6.0.0 does not enable it by default; check the actual installed package’s rule file, don’t assume from the name.
  • Code that only “compiles” because of an implicit dynamic somewhere — proven above, strict-casts/strict-inference/strict-raw-types catch exactly this class of bug, but only if a project’s analysis_options.yaml actually turns them on (they are off by default even with flutter_lints included).

Every layer in this lesson is a different kind of “what did I actually agree to, versus what did I assume.” pubspec.yaml is an explicit contract for what ships. A lint package’s name (flutter_lints, very_good_analysis) is not itself the contract — the actual .yaml rule file it resolves to is, and reading it directly (as this lesson did, finding the prefer_const_constructors gap) is the only way to know for certain what’s enforced. strict-casts/strict-inference/strict-raw-types exist because Dart’s type system has an escape hatch (dynamic) that’s silent by default — these three flags are how you tell the analyzer “stop letting me use it by accident.” And dart:io on web is the sharpest version of this lesson: the compiler’s silence (a successful build) is not the same thing as safety, proven above by a build that genuinely succeeds while shipping a guaranteed runtime crash.

ChoiceBenefitCost
flutter_lints (default from flutter create)Small, low-friction, Flutter-specific — proven above, ten rules on top of lints/recommended.yamlProven above: does not enforce prefer_const_constructors or several other performance/safety rules very_good_analysis does
very_good_analysisMuch stricter baseline (237 lines of rules) — proven above to include prefer_const_constructorsMore upfront friction on an existing codebase; likely surfaces many pre-existing violations at once
strict-casts/strict-inference/strict-raw-types onCatches real implicit-dynamic bugs at analyze time, proven above with three real diagnosticsMore explicit type annotations required throughout the codebase
Conditional import/export for platform-specific codeProven above: the non-selected implementation’s code (and its imports) genuinely isn’t part of that platform’s bundleMore files/boilerplate than a single if (kIsWeb) branch
kIsWeb runtime guard aloneSimple, one file, no extra structureProven above: does not remove the guarded code (or its imports) from the web bundle — only prevents it from running
  • “Including flutter_lints means prefer_const_constructors is enforced.” Disproven above by reading flutter_lints 6.0.0’s actual rule file: it ships only the narrower prefer_const_constructors_in_immutables.
  • “strict-casts/strict-inference/strict-raw-types are lints, so a lint package like flutter_lints turns them on.” Disproven above: they are analyzer language options, configured under a completely different YAML key (analyzer: language:), off by default regardless of which lint package is included.
  • “If dart fix --dry-run finds a diagnostic, it can fix it.” Disproven above: avoid_unnecessary_containers is a real, live diagnostic with zero proposed auto-fixes on the exact same file where use_full_hex_values_for_flutter_colors had one.
  • “flutter build web will fail to compile if the code imports dart:io.” Disproven above with a real build: it succeeds, even calling Platform.isIOS and File.readAsString() from main() — the failure, proven from source, happens only at runtime, in the browser, the first time that code path actually executes.
  • “A kIsWeb check is enough to make dart:io-touching code web-safe.” Disproven above: the code (and its dart:io import) is still compiled into the web bundle either way; only a conditional import/export changes which implementation — and which imports — are actually present per platform.
What is the difference between dependencies and dev_dependencies in pubspec.yaml?
Proven above by reading the actual installed package sources, what is the real gap between flutter_lints 6.0.0 and very_good_analysis 11.0.0?
Proven above with three real diagnostics, what do strict-casts/strict-inference/strict-raw-types actually catch?
Proven above, what happens when you run flutter build web on code that directly imports dart:io and calls File.readAsString()?
Proven above, why is a conditional import/export more robust than a bare if (kIsWeb) check for dart:io-touching code?
Proven above, does dart fix --dry-run propose a fix for every diagnostic flutter analyze reports?