Lesson 01 — Windows Environment Setup
NomadAir Masterclass · Module 1 · Beginner
The Naive Flutter Approach — And Why It Breaks
The beginner playbook for Flutter on Windows goes like this: download the SDK, run flutter doctor, fix the red items until everything is green, create a project, and assume you are ready. This is wrong in a specific way that will cost you hours the first time it hits.
flutter doctor checks for the existence of tools, not for their compatibility with each other. It finds a JDK — it reports green. It finds Android Studio — it reports green. It finds the Flutter SDK — it reports green. But when you run flutter build apk for the first time, Gradle silently starts its own dependency resolution process, and that is where environments collapse.
The three failure patterns that flutter doctor will never catch:
Java/Gradle mismatch. Flutter 3.35+ uses Gradle 8.x internally. Gradle 8.x requires Java 17. If your JAVA_HOME points to a system Java 21 installation with a module system that breaks certain Gradle plugins, your first build fails with Execution failed for task ':app:compileDebugKotlin' — not a meaningful error for a new developer.
Android license state. Running flutter doctor --android-licenses once is insufficient if Android Studio later updates the SDK. Newly downloaded API levels sometimes arrive with un-accepted license agreements that block ADB from deploying to an AVD.
AVD architecture mismatch. On Windows with an AMD CPU, selecting a Google APIs x86_64 image works. On Windows with an ARM chip, you need a different image. Getting this wrong produces an AVD that boots but where flutter run hangs at Installing build/app/outputs/flutter-apk/app-debug.apk.
None of these appear until you actually build. This lesson introduces a verification scaffold that runs on the device itself and makes every check observable in real time.
What Flutter Is Actually Doing
When you run flutter run, the command you typed is the beginning of a six-stage pipeline:
Flutter CLI reads
pubspec.yaml, resolves the pub dependency graph, and invokes the Dart compiler to produce kernel bytecode (a.dillfile).Gradle picks up the kernel bytecode, reads
android/app/build.gradle, resolves the Android Gradle Plugin version, and begins compiling Kotlin/Java bridge code.D8/R8 (the Android bytecode compiler, embedded in the AGP) compiles Dart's kernel bytecode alongside the Kotlin bridge into DEX format for the Android Runtime (ART).
APK packaging bundles the DEX files, the Flutter engine
.solibraries, and your app's assets into an APK.ADB pushes the APK to the connected AVD via a TCP socket (port 5037 by default).
ART on the AVD JIT-compiles the DEX bytecode on first run. Subsequent runs benefit from an AOT-compiled profile cache.
The environment check screen in this lesson runs inside step 6. By the time your Dart code executes on the AVD, all six stages have succeeded. That is the correct definition of a passing environment.
Understanding this pipeline also explains why hot reload is faster than a full restart: it only re-runs from step 1 (updated kernel bytecode) through ADB, skipping the Gradle and D8/R8 stages entirely.
The NomadAir Architecture for This Feature
Every screen in NomadAir is built from three layers of the feature-folder structure:
The domain type for this lesson is EnvCheckResult — a sealed class with four concrete subtypes: CheckPending, CheckRunning, CheckPassing, and CheckFailing. Sealed classes in Dart 3.x enforce exhaustive pattern matching at every switch site. If a fifth state is ever added, every switch expression in the codebase that handles EnvCheckResult becomes a compile error until it handles the new case. This is the architecture enforcing correctness — not a comment or a convention.
The cross-cutting concern this lesson also establishes is the NomadAir design token system: AppColors, AppTypography, and AppSpacing. These are abstract final classes — not instantiable, not subclassable — that hold the single source of truth for every visual constant in the app. NomadAirTheme consumes them to produce a ThemeData for both light and dark variants.
Implementation Deep Dive
The core Dart 3.x pattern in this lesson is the sealed class combined with a record destructuring switch expression.
In CheckTile, a single switch expression simultaneously selects the icon, the color, and the subtitle text based on the check's current state:
The (icon, color, subtitle) on the left is a Dart 3.x record destructuring: a positional tuple of (IconData, Color, String) that unpacks the switch result into three named local variables in a single statement. This replaces what would otherwise be three separate if-else chains.
The if-case pattern on the failing state renders the fix hint only when the result is a CheckFailing:
This is cleaner than a null check or a type cast because it binds the fix property and tests the type simultaneously, and it does not compile if CheckFailing is ever renamed.
The async check pipeline in _EnvCheckScreenState deserves attention. Each of the seven checks follows the same pattern:
setState(() => _results[index] = CheckRunning(...))— the UI immediately shows the running spinner.A 300ms artificial delay simulates network or platform latency.
The actual platform query executes:
Platform.isAndroid,DeviceInfoPlugin().androidInfo,PackageInfo.fromPlatform().setState(() => _results[index] = result)— the UI transitions to passing or failing.
After every await that touches BuildContext, a if (!mounted) return guard prevents calling setState on a disposed widget. This is the correct pattern for async operations that span multiple frames in a StatefulWidget.
Production Readiness
Three things to confirm after running this lesson's project on the AVD:
flutter analyze exits with code 0. Run it from the project directory. Any lint violation is a warning about code quality in a codebase with 90 lessons ahead. Fix them before proceeding.
The Performance Overlay shows consistent green bars. Open it via the Flutter Inspector's "Toggle Performance Overlay" button. On a Pixel 7 API 34 AVD with no background apps, the env check screen should hold 60fps throughout the check sequence. If it doesn't, your AVD's hardware acceleration is not configured correctly.
Logcat shows no uncaught Dart exceptions. Filter by your app's package name (com.nomadair.lesson01) in the Logcat panel. A red E/flutter entry with a stack trace means something threw without being handled by the Result type you will build in Lesson 24.
Step-by-Step Guide
Prerequisites
Windows 10 or 11 (64-bit)
Flutter SDK 3.35+ in
PATH(verify:flutter --version)Android Studio Hedgehog or later with the Flutter and Dart plugins
Pixel 7 API 34 AVD created in Android Studio's AVD Manager
Python 3.11+ in
PATH(verify:python --version)
Execution
Place generate_lesson_01.py in any working directory. Then run:
The script will:
Verify Flutter is installed and print its version.
Run
flutter create nomadair_lesson_01 --org com.nomadair --platforms android,ios.Write all NomadAir Dart source files, overriding the counter-app defaults.
Update
pubspec.yamlwithdevice_info_plusandpackage_info_plus.Run
flutter pub get.Print a file-by-file generation log with byte counts.
Open the generated nomadair_lesson_01/ folder in Android Studio via File → Open.
Start your Pixel 7 API 34 AVD, then run:
Or from within the project directory:
Android Verification
Tap Run Checks in the app. All seven tiles should transition from grey pending icons to green passing icons. The log panel should show seven timestamped PASS entries. The status bar at the top should show 7 Passed / 0 Failed.
Tap Inject Failure to observe the Screen Dimensions tile transition to a red failing state with a fix hint. This demonstrates the CheckFailing state and the pattern-matched rendering in CheckTile.
Run the test suite:
All unit and widget tests should pass. Then run the integration test on the connected AVD:
iOS Verification (Optional — macOS Only)
If you are on macOS and have Xcode installed:
All seven checks should pass identically, with "Device Info" now reporting iOS version and device model.