Installing Flutter and Your First Project: the SDK, Android Studio, Xcode, Emulators, flutter doctor and Hot Reload
This post covers installing Flutter on Windows for Android and on macOS for Android and iOS, reading the output of flutter doctor and creating bookstore_app with material_ui. We show with a real run that hot reload keeps state while hot restart resets it, map pubspec.yaml to csproj and package.json, and verify the app without an emulator using a widget test and a web build.
Version note: The examples in this post were run with Flutter 3.47.8 (Dart 3.13.7) and material_ui 1.11.0. The web build was checked in Playwright 1.65's headless Chromium. The Android emulator, iOS simulator and real-device steps were not run for this post; they are described from the documentation and marked where they appear.
Guess First
In this post we create the mobile app of Bookstore, the online bookstore used as the shared example across this site's series. The first screen is simple: a "Bookstore" title at the top, the text "Books in cart: 0" in the middle and a button at the bottom right that adds a book to the cart.
While the app runs in debug mode with flutter run, you press the button twice; the screen says "Books in cart: 2". Now you change that text in the code to "In your cart: 2 book(s)" and press r in the terminal (hot reload). What does the screen show: the new text and 2, the new text and 0, or the old text? And what changes if you then press R (hot restart)? Guess the answer; in the middle of the post we actually run the app and measure it.
What Can You Build on Which Operating System?
The Flutter SDK runs on Windows, macOS and Linux, but the platforms you can build for depend on the operating system. Building, running on the simulator and signing an iOS app requires Xcode, and Xcode only runs on macOS. So:
- Windows: Android (emulator and real device), web and Windows desktop. No iOS.
- macOS: Android, iOS, web and macOS desktop. The most complete environment for the series.
- Linux: Android, web and Linux desktop.
If you work on Windows like most readers, you can follow the Android side of the series completely. When you want to see iOS, you need a Mac or a CI machine running macOS; in the publishing post we cover iOS builds on GitHub Actions' macOS runners. One note on the Mac side: Flutter is gradually ending support for Intel-based Macs, and macOS 27 already requires Apple Silicon. If you are buying a new Mac, that makes the decision easier.
Installing the SDK and flutter doctor
The Flutter documentation gives two ways to install. The recommended way is VS Code: install the Flutter extension, and when you start a new project from the command palette the extension downloads the SDK, adds it to PATH and validates the setup itself. The second way is manual: download the stable SDK archive for your operating system, extract it into a folder and add its bin folder to PATH. On Windows, extracting the SDK somewhere without spaces that does not need administrator rights, such as C:\src\flutter, avoids problems later; C:\Program Files is a bad choice.
Android needs Android Studio, because it installs the Android SDK, the emulator and the build tools. You are asked to select the API 36 platform and the Build-Tools, Command-line Tools, Platform-Tools, Emulator, CMake and NDK packages in the SDK Manager. Then, to accept the licenses:
flutter doctor --android-licenses
This command shows the Android SDK licenses one by one and asks for approval; we did not run it for this post because the setup was already complete. For the emulator you create an AVD in the Device Manager; choosing a system image that matches your computer's architecture (x64 or Arm64) and setting graphics acceleration to hardware are essential for the emulator to run at a usable speed.
On macOS, after installing Xcode for iOS, you select the command-line tools, run the first-launch steps, accept the license and download the iOS platform (including the simulator runtime):
sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer && xcodebuild -runFirstLaunch'
sudo xcodebuild -license
xcodebuild -downloadPlatform iOS
Because some plugins with native code still need CocoaPods, the setup page also tells you to install CocoaPods. These steps change system settings and ask for license approval, so they were not run again in this post's environment.
You see the state of the setup with one command:
flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.47.8, on macOS 27, locale tr-TR)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 27.2)
[✓] Chrome - develop for the web
[✓] Connected device (3 available)
[✓] Network resources
• No issues found!
We shortened the operating system information on the first line; the rest is real output. Each line is a validator: [✓] means no problem, [!] a warning (often a missing license or an outdated tool), and [✗] means you cannot build for that platform. On Windows, instead of the Xcode line you get a "Visual Studio - develop Windows apps" line for Windows desktop; a warning on that line only affects the Windows desktop target, not Android. There is no Android Studio line on this machine because we ran the command with an isolated user settings folder and doctor looks for Android Studio there; the Android toolchain line needed for Android builds is fine. When a line has a problem, flutter doctor -v shows what each validator checks and which path it found.
You can use VS Code or Android Studio as the editor; both have Flutter and Dart extensions that support hot reload, the widget inspector and widget previews. For a .NET developer coming from Visual Studio or Rider, VS Code is usually the lighter start.
Creating the bookstore_app Project
The series app is bookstore_app; Dart package names must be snake_case, and bookstore-app is an invalid name. We limit the target platforms from the start: Android and iOS are the main targets, the web is only for quick checks in a browser.
flutter create --org com.example --platforms android,ios,web bookstore_app
Creating project bookstore_app...
Resolving dependencies in `bookstore_app`...
Downloading packages...
Got dependencies in `bookstore_app`.
Wrote 82 files.
All done!
--org sets the prefix of the app identity: the applicationId on Android and the bundle identifier on iOS became com.example.bookstore_app. This value cannot be changed once the app is uploaded to a store; in a real app you use your company's domain name in reverse. The important part of the generated structure:
bookstore_app/
lib/main.dart
test/widget_test.dart
android/
ios/
web/
pubspec.yaml
pubspec.lock
analysis_options.yaml
lib/main.dart is the app's entry point (main and runApp), and test/widget_test.dart is a sample widget test. android/ is a Gradle project (applicationId, minSdk, signing), ios/ is an Xcode project (bundle id, Info.plist, deployment target), and web/ holds index.html and the manifest. pubspec.yaml contains the package name, version, dependencies and assets, pubspec.lock the resolved package versions, and analysis_options.yaml the lint rules (flutter_lints).
All Dart code lives under lib/. The android/ and ios/ folders are real Gradle and Xcode projects; Flutter uses them during the build, and that is where you set platform settings such as permissions, icons, the app name and signing. Most days you do not touch these folders, but you do not delete them either: the settings in them are yours.
Minimum platform versions
You can see which Android and iOS versions you support in the generated files. android/app/build.gradle.kts takes the values from Flutter's Gradle plugin:
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
In the Gradle plugin of Flutter 3.47.8 these values are minSdkVersion = 24, targetSdkVersion = 36 and compileSdkVersion = 36. On the iOS side, ios/Runner.xcodeproj/project.pbxproj says IPHONEOS_DEPLOYMENT_TARGET = 15.0. This detail matters: the 3.47 announcement says the iOS minimum went from 13 to 15, but on the day of writing Flutter's iOS release documentation still says "iOS 13 and later". The value in the generated project and the supported platforms page confirm 15; that sentence in the documentation is out of date.
pubspec.yaml: the Counterpart of csproj and package.json
pubspec.yaml does in one file what .csproj does in .NET and package.json does in npm. With the comments removed:
name: bookstore_app
description: "Bookstore customer mobile app (catalog, book details, cart, orders, sign-in)."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.13.7
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
material_ui: ^1.11.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
version: 1.0.0+1 has two parts: the part before + is the version the user sees (versionName on Android, CFBundleShortVersionString on iOS), and the part after it is the build number the store expects to increase with every upload (versionCode, CFBundleVersion). environment.sdk resembles TargetFramework in .NET but specifies a range. ^1.11.0 means "1.11.0 or later, below 2.0.0", the same meaning as npm's caret. dev_dependencies are packages used only in development and tests, the counterpart of the packages you add to a test project in .NET.
pubspec.lock holds the exact resolved version of every package. In app projects it is committed, so CI and everyone on the team build with the same versions; it does what packages.lock.json does in .NET and package-lock.json does in npm. To add a package, instead of editing the file by hand:
flutter pub add material_ui
Resolving dependencies...
Downloading packages...
cupertino_icons 1.0.9 (2.0.0 available)
+ cupertino_ui 1.1.6
+ flutter_localizations 0.0.0 from sdk flutter
+ intl 0.20.3
material_color_utilities 0.13.0 (0.13.1 available)
+ material_ui 1.11.0
meta 1.18.3 (1.19.0 available)
test_api 0.7.12 (0.7.15 available)
vector_math 2.4.0 (2.4.4 available)
Changed 4 dependencies!
5 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
Lines starting with + are newly added packages; the point to notice is that material_ui also brings cupertino_ui, flutter_localizations and intl. Transitive dependencies are resolved automatically, as in NuGet. The "available" lines say that some packages pinned by the Flutter SDK have newer versions; those are updated with the SDK, so do not try to upgrade them by hand.
material_ui and the First Screen
The flutter create template of Flutter 3.47.8 still comes with the package:flutter/material.dart import. In the first post we decided to use the material_ui package; instead of changing the imports by hand, we run the automated fix Flutter provides:
dart fix --apply --code=migrate_design_widgets
Computing fixes in bookstore_app...
Applying fixes...
lib/main.dart
migrate_design_widgets - 1 fix
test/widget_test.dart
migrate_design_widgets - 1 fix
2 fixes made in 2 files.
One trap: dart fix --dry-run without --code said "Nothing to fix!" on the same project. This fix is not part of the general dart fix scan; you have to pass the code explicitly. We deleted the template's counter sample and split Bookstore's first screen into three files. lib/main.dart only starts the app:
import 'package:material_ui/material_ui.dart';
import 'app.dart';
void main() {
runApp(const BookstoreApp());
}
lib/app.dart is the root of the app; the theme and the first screen are here:
import 'package:material_ui/material_ui.dart';
import 'ui/home/widgets/home_screen.dart';
class BookstoreApp extends StatelessWidget {
const BookstoreApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bookstore',
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo)),
home: const HomeScreen(),
);
}
}
The folder structure follows the layout in Flutter's official architecture guide: screens under lib/ui/<feature>/widgets/, and later view models under lib/ui/<feature>/view_models/ and repositories and services under lib/data/. For now HomeScreen is a StatefulWidget that keeps its own counter:
import 'package:flutter/widget_previews.dart';
import 'package:material_ui/material_ui.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _booksInCart = 0;
void _addBook() {
setState(() => _booksInCart++);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Bookstore')),
body: Center(
child: Text(
'Books in cart: $_booksInCart',
style: Theme.of(context).textTheme.headlineSmall,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _addBook,
tooltip: 'Add book',
child: const Icon(Icons.add_shopping_cart),
),
);
}
}
@Preview(name: 'Home screen')
Widget homeScreenPreview() => const MaterialApp(home: HomeScreen());
We open up StatefulWidget, setState and BuildContext in detail in the sixth post. For now, what you need to know: the value of _booksInCart lives not in the widget itself but in the State object attached to it, and calling setState tells Flutter "draw this screen again". It is similar to changing a field of an Angular component and triggering change detection.
Hot Reload, Hot Restart and Full Restart
Normally you open the app with flutter run on an Android emulator or the iOS simulator. In this post's environment the emulator and simulator were not run; we set up the same debug loop with Flutter's web-server device, opened the screen in Playwright's headless Chromium and tapped the button from a script:
flutter run -d web-server --web-hostname 127.0.0.1 --web-port 52818
Launching lib/main.dart on Web Server in debug mode...
Waiting for connection from debug service on Web Server... 12,5s
lib/main.dart is being served at http://127.0.0.1:52818
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
The script pressed the button twice, then changed the text in home_screen.dart to 'In your cart: $_booksInCart book(s)' and sent first r, then R. The texts read from the screen and the output of flutter run:
before edit : Books in cart: 2
after r : In your cart: 2 book(s)
after R : In your cart: 0 book(s)
Performing hot reload... 80ms
Reloaded application in 80ms.
Performing hot restart... 65ms
Restarted application in 65ms.
The answer to "Guess First": hot reload loaded the new code and rebuilt the widget tree but did not touch the State object; the counter stayed at 2 and the text changed. Hot restart loaded the new code and started the app from scratch; main() ran again and the state was reset.
It is important to know what hot reload does not run again. main() and initState() do not run again; the initial values of global variables and static fields count as state, and changing them needs a hot restart. Turning an enum into a class or changing the number of type parameters of a generic type is not applied by hot reload. Native code such as Kotlin or Swift, or adding a new plugin to pubspec.yaml, needs the app to be stopped completely and rebuilt (a full restart). Hot reload only exists in debug mode; profile and release builds do not have it.
Widget Previews: looking at one widget in isolation
The @Preview annotation above is for Widget Previews, which became stable in 3.47. It comes from package:flutter/widget_previews.dart and goes on a top-level or static function, or on a public constructor without required parameters. flutter widget-preview start, or the "Flutter Widget Preview" panel in VS Code and Android Studio, shows these widgets one by one without running the whole app; with parameters such as size, brightness and textScaleFactor you can see the same widget side by side under different conditions. Because the previewer runs on Flutter web and opens a browser, it was not started in this environment; flutter analyze confirmed that the annotation compiles. Mind its limits: code inside a preview cannot call dart:io or native plugin APIs.
Checking Without an Emulator: a Widget Test and a Web Build
You do not have to open an emulator for every change. Flutter's widget tests run quickly without a device. We rewrote the template's counter test for the Bookstore screen:
import 'package:bookstore_app/app.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('tapping the button adds a book to the cart', (tester) async {
await tester.pumpWidget(const BookstoreApp());
expect(find.text('Books in cart: 0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add_shopping_cart));
await tester.pump();
expect(find.text('Books in cart: 1'), findsOneWidget);
});
}
flutter analyze
flutter test
Analyzing bookstore_app...
No issues found! (ran in 4.8s)
00:00 +0: loading test/widget_test.dart
00:00 +0: tapping the button adds a book to the cart
00:00 +1: All tests passed!
The test builds the app as it would on a real device and taps the button; the only difference is that the screen is not visible. In the testing post we extend this with view models and fake repositories. The second check is a web build in release mode:
flutter build web
Compiling lib/main.dart for the Web...
Wasm dry run succeeded. Consider building and testing your application with the `--wasm` flag. See docs for more info: https://docs.flutter.dev/platform-integration/web/wasm
Use --no-wasm-dry-run to disable these warnings.
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 257628 to 1472 bytes (99.4% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7916 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Compiling lib/main.dart for the Web... 17,2s
✓ Built build/web
The icon fonts shrinking by more than 99 percent through tree shaking shows that the release build drops unused code and icons. When we serve build/web from a local HTTP server and open it with Playwright:
title: Bookstore
flutter-view elements: 1
native <button> elements before semantics: 0
semantics text found: Books in cart: 0
after click: Books in cart: 1
console errors: 0
This is the measured form of the claim in the first post: there is not a single HTML button on the page; everything is inside the flutter-view that Flutter draws. To find the text and the button we first had to make Flutter turn on its semantics tree; this tree lets screen readers and test tools "see" the app. On mobile the same tree goes to TalkBack and VoiceOver.
In Production
Pin the Flutter SDK version in the team. Everyone having a different stable release on their machine leads to the same commit compiling on one machine and not on another. Adding a range such as flutter: '>=3.47.8 <3.48.0' to the environment section of pubspec.yaml (since Dart 3.9 pub also respects the upper bound of that range) and selecting the version explicitly in CI prevents this. It is similar to what global.json does in .NET.
Choose the applicationId and the bundle identifier correctly from day one. You cannot upload to a store with com.example, and once uploaded the identity cannot be changed; changing it means publishing a new app.
android/local.properties and ios/Flutter/Generated.xcconfig contain machine-specific paths. The template's .gitignore already leaves them out; make sure they are not added to the repo, otherwise another developer's build looks for the SDK path on your machine.
Common Pitfalls
The first pitfall is losing hours because "the change does not show" after a hot reload. If the line you changed is in initState, in main() or in the initial value of a global variable, hot reload does not run it. When in doubt, hot restart with R.
The second pitfall is treating every warning in flutter doctor as a blocker. On Windows the warning on the Visual Studio line only affects the Windows desktop build; if you are developing for Android you can ignore it. What matters is that the line for the platform you target is green.
The third pitfall is running the Android emulator with software graphics. If graphics acceleration is not set to hardware in the Device Manager, the emulator becomes so slow that you think your app is slow.
The fourth pitfall is going back to the old Material import. If an example you copied from the internet imports package:flutter/material.dart, mixing it with material_ui in the same file leads to clashing type names. After taking the example, run dart fix --apply --code=migrate_design_widgets again.
A Small Experiment
★☆☆ Create bookstore_app on your own machine, check which SDK path the Android toolchain line finds in the flutter doctor -v output, and run the app on an Android emulator.
★★☆ While the app is running, change the color in ColorScheme.fromSeed to Colors.teal and try it with hot reload. Then make a change in main() by passing a different widget to runApp. Which one shows with hot reload, and which one needs a hot restart?
★★★ Add two more previews for HomeScreen: one with brightness: Brightness.dark and one with textScaleFactor: 2.0. Do you see an overflow in the title or the button at the large text size? If so, note how we will solve it in the layout post.
In the next post we leave Flutter for a while and move to Dart: types for someone who knows C#, the difference between var, final and const, sound null safety, collections and records, and the first version of Bookstore's Book and Author models.