Day 1: Windows Environment Setup

Lesson 1 60 min

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:

  1. Flutter CLI reads pubspec.yaml, resolves the pub dependency graph, and invokes the Dart compiler to produce kernel bytecode (a .dill file).

  2. Gradle picks up the kernel bytecode, reads android/app/build.gradle, resolves the Android Gradle Plugin version, and begins compiling Kotlin/Java bridge code.

  3. 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).

  4. APK packaging bundles the DEX files, the Flutter engine .so libraries, and your app's assets into an APK.

  5. ADB pushes the APK to the connected AVD via a TCP socket (port 5037 by default).

  6. 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

Component Architecture

Lesson 01 — Flutter Toolchain on Windows Component Architecture 🖥 Developer Machine (Windows) Android Studio IDE Flutter Plugin Hot Reload · Inspector · Profiler Dart Plugin Syntax · Analysis · Formatting flutter run / flutter build Flutter SDK 3.35+ Flutter CLI Dart Compiler (CFE) pub (packages) Flutter Engine (.so) Gradle build · AGP 8.x Android Build System Gradle 8.x · Java 17 D8 / R8 (DEX compiler) APK Packager · ADB (5037) adb install · APK push Android AVD — Pixel 7 API 34 ART Runtime (JIT → AOT) Dart VM on device EnvCheckResult runs HERE flutter doctor checks tool existence — not compatibility Java 21 + Gradle 8 = build failure · Java 17 + Gradle 8 = success

Every screen in NomadAir is built from three layers of the feature-folder structure:

Code
lib/features/env_check/
├── models/    ← domain types (EnvCheckResult sealed class)
├── screens/   ← stateful screen widget
└── widgets/   ← stateless building blocks (CheckTile)

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

Flowchart

Async Check Pipeline — Data Flow User tap → setState(Running) → platform query → setState(result) → UI + Log USER WIDGET STATE ASYNC CHECKS PLATFORM API UI + LOG Tap "Run Checks" setState() reset all → Pending results[0] → Running _exec(i, checkFn) await 280ms delay await checkFn() dart:io Platform.* device_info_plus package_info_plus Success path Error path CheckPassing value: 'ANDROID' CheckFailing error: '...' fix: '...' setState() results[i] = result setState() results[i] = result CheckTile 🟢 Passing CheckTile 🔴 Failing Loop: repeat for checks 1–6 LOG [11:03:22.110] Checking: Platform Target... [11:03:22.394] Platform Target: PASS — ANDROID [11:03:22.678] Screen Dimensions: FAIL — 320x480 dp — below minimum Real-time timestamped log (milliseconds) if (!mounted) return ← async guard

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:

dart
final (icon, color, subtitle) = switch (result) {
  CheckPending()               => (Icons.radio_button_unchecked, Colors.grey,       'Waiting...'),
  CheckRunning()               => (Icons.sync,                   AppColors.info,    'Running...'),
  CheckPassing(value: final v) => (Icons.check_circle,           AppColors.success,  v),
  CheckFailing(error: final e) => (Icons.cancel,                 AppColors.error,    e),
};

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:

dart
if (result case CheckFailing(fix: final f)) ...[
  Text('Fix: $f', style: ...),
],

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:

  1. setState(() => _results[index] = CheckRunning(...)) — the UI immediately shows the running spinner.

  2. A 300ms artificial delay simulates network or platform latency.

  3. The actual platform query executes: Platform.isAndroid, DeviceInfoPlugin().androidInfo, PackageInfo.fromPlatform().

  4. 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

State Machine

EnvCheckResult — Sealed Class State Machine Dart 3.x sealed hierarchy · exhaustive switch · no runtime casts sealed class EnvCheckResult name: String · description: String Check Pending ⬤ Waiting final class Check Running ↻ Async active final class Check Passing ✓ value: String TERMINAL final class · terminal Check Failing ✕ error: String fix: String TERMINAL final class · terminal run checks success error reset / run again → all → Pending final (icon, color, label) = switch (result) { CheckPending() => (Icons.circle, Colors.grey, 'Waiting...'), CheckPassing(value: final v) => (Icons.check, AppColors.success, v), ← Dart 3 record

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:

Code
python generate_lesson_01.py --generate

The script will:

  1. Verify Flutter is installed and print its version.

  2. Run flutter create nomadair_lesson_01 --org com.nomadair --platforms android,ios.

  3. Write all NomadAir Dart source files, overriding the counter-app defaults.

  4. Update pubspec.yaml with device_info_plus and package_info_plus.

  5. Run flutter pub get.

  6. 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:

Code
python generate_lesson_01.py --run

Or from within the project directory:

Code
flutter run -d emulator-5554

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:

Code
python generate_lesson_01.py --test

All unit and widget tests should pass. Then run the integration test on the connected AVD:

Code
flutter test integration_test/ -d emulator-5554

iOS Verification (Optional — macOS Only)

If you are on macOS and have Xcode installed:

Code
python generate_lesson_01.py --ios

All seven checks should pass identically, with "Device Info" now reporting iOS version and device model.

Questions & Discussion

Leave a Reply

Your email address will not be published. Required fields are marked *

System Design Fundamentals – E-Book

Free download

Free eBook: System Design Fundamentals

Create a free account and download the ebook instantly. Learn the core building blocks — scaling, caching, databases and messaging — the way interviewers expect you to explain them.

Register free & download →

Already a member? Sign in to download · See what’s inside