Before writing meaningful Flutter apps, you need a solid grasp of the development environment and the foundational concepts that govern every Flutter project. These basics form the mental model you will build on throughout your Flutter journey.
Preparing the Environment
After installing the Flutter SDK, you can verify and configure your setup using the flutter CLI.
# Check the full Flutter environment for any issues
flutter doctor
# List all connected and available devices
flutter devices
# Enable web support for an existing project
flutter config --enable-web
To target additional platforms in an existing project, use the flutter create command with a platform flag:
# Add web support to current project
flutter create --platform web .
# Add iOS support to current project
flutter create --platform ios .
Creating a Project
flutter create my_first_app
This generates a complete project structure with all the necessary files for a cross-platform Flutter application.
my_first_app/
├── lib/
│ └── main.dart <-- Entry point
├── android/ <-- Android platform code
├── ios/ <-- iOS platform code
├── web/ <-- Web platform code
├── test/ <-- Test files
├── pubspec.yaml <-- Dependencies and configuration
└── README.md
Build and Run
In VS Code, select the target device from the bottom status bar and press F5 to run with the debugger attached.
From the command line:
# Clean previous build artifacts
flutter clean
# Fetch all declared dependencies
flutter pub get
# Run on the currently connected device
flutter run
# Build a release APK for Android
flutter build apk --release
# Build a release bundle for iOS
flutter build ios --release
Hello World — The Minimal Flutter App
Every Flutter application starts with the runApp() function. This takes a widget and makes it the root of the widget tree.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Hello World')),
body: const Center(
child: Text('Hello, Flutter!', style: TextStyle(fontSize: 24)),
),
);
}
}
Key observations:
main()is the Dart entry point for every applicationrunApp()attaches the provided widget as the root of the UIMaterialAppsets up the Material Design system and routingScaffoldprovides the standard page layout: app bar, body, floating action button- Everything is a widget — even layout, spacing, and styling
Flutter Design System
Flutter uses the Material package to draw all widgets. You choose the design language based on your target platform conventions.
| Design System | Target Platform | Package |
|---|---|---|
| Material Widgets | Android (and cross-platform) | package:flutter/material.dart |
| Cupertino Widgets | iOS native look and feel | package:flutter/cupertino.dart |
Both can be used in the same app. Material widgets work well on all platforms, while Cupertino widgets provide the native iOS appearance.
Widget Types
All Flutter UI is composed of two fundamental widget types:
Widget
├── StatelessWidget (immutable, no internal state)
└── StatefulWidget (mutable, can rebuild on state change)
Stateless Widgets
A StatelessWidget cannot change its appearance after it is built. It has no internal mutable state — its entire UI is determined by the properties (parameters) passed to it at construction time.
Use stateless widgets for content that does not change: text labels, icons, static layouts.
In VS Code, type stl and press Tab to generate the boilerplate:
import 'package:flutter/material.dart';
class ProfileCard extends StatelessWidget {
final String name;
final String role;
const ProfileCard({super.key, required this.name, required this.role});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text(name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(role, style: const TextStyle(color: Colors.grey)),
],
),
);
}
}
Stateful Widgets
A StatefulWidget has mutable state managed by a separate State object. When you call setState(), Flutter rebuilds only the affected parts of the widget tree.
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count', style: const TextStyle(fontSize: 24)),
ElevatedButton(onPressed: _increment, child: const Text('Increment')),
],
);
}
}
Flutter Project Lifecycle
A complete Flutter project follows a structured lifecycle from concept to deployment.
Plan --> Design --> Set Up Environment --> Develop --> Test --> Optimize --> Deploy
Planning
- Clarify objectives and goals
- Identify the target audience
- List required features
- Create wireframes and user flows
Designing
- Define the user experience (UX) flow
- Design the user interface (UI)
- Map screen connections and app navigation
- Arrange pre-built and custom widgets
Environment Setup
- Install the Flutter SDK from flutter.dev
- Choose an IDE: VS Code or Android Studio
- Configure device emulators or physical devices
- Run
flutter doctorto verify the environment
Development
The development phase involves four iterative activities:
- Dart coding — implement business logic in Dart
- Build UI with widgets — compose stateless, stateful, and layout widgets
- Implement features — integrate APIs, local storage, and device capabilities
- Testing and debugging — write unit, widget, and integration tests
Optimization
- Reduce app size
- Improve rendering performance
- Ensure compatibility across target devices and OS versions
Deployment
- Build signed release packages for iOS and Android
- Submit to the Apple App Store and Google Play Store
Choosing Between StatelessWidget and StatefulWidget
A simple rule of thumb:
Does this widget need to change after it is first built?
|
+-- No --> StatelessWidget (simpler, faster)
|
+-- Yes --> StatefulWidget (use setState or state management)
Start with StatelessWidget and only introduce StatefulWidget when you have a clear need for mutable state. For app-wide or shared state, prefer a dedicated state management solution like Provider or Riverpod over raw setState.