Flutter is Google's open-source UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase. With over 2 million developers, 175,000+ apps on the Play Store alone, and backing from Google, Flutter has become the most popular cross-platform framework. This guide takes you from installation to publishing on the Play Store.
Installation: Windows, Mac, and Linux
Flutter requires the Flutter SDK, an IDE (VS Code or Android Studio), and platform-specific tools for the platform you are targeting.
Windows Installation
# Option 1: Download from flutter.dev
# Download the latest stable Flutter SDK zip from https://flutter.dev/docs/get-started/install/windows
# Extract to C:flutter (avoid paths with spaces or special characters)
# Option 2: Using Chocolatey
choco install flutter
# Add Flutter to PATH
# System Properties > Environment Variables > Path > Add: C:flutterin
# Verify installation
flutter --version
# Flutter 3.27.x • channel stable
# Run Flutter Doctor to check dependencies
flutter doctor -v
Mac Installation
# Using Homebrew (recommended)
brew install flutter
# Or manual download
cd ~/development
git clone https://github.com/flutter/flutter.git -b stable
# Add to PATH in ~/.zshrc
export PATH="$PATH:$HOME/development/flutter/bin"
source ~/.zshrc
# Install Xcode (for iOS development)
xcode-select --install
sudo xcodebuild -license accept
# Install CocoaPods
sudo gem install cocoapods
flutter doctor -v
Linux Installation
# Install dependencies
sudo apt update
sudo apt install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build
# Download Flutter
cd ~
git clone https://github.com/flutter/flutter.git -b stable
# Add to PATH in ~/.bashrc
echo 'export PATH="$PATH:$HOME/flutter/bin"' >> ~/.bashrc
source ~/.bashrc
flutter doctor -v
Fixing Common flutter doctor Issues
After running flutter doctor, you will likely see warnings. Here is how to fix each one:
# Issue: Android toolchain - cmdline-tools component is missing
# Fix: Open Android Studio > SDK Manager > SDK Tools tab > Check "Android SDK Command-line Tools"
# Issue: Android license status unknown
flutter doctor --android-licenses
# Press 'y' to accept all licenses
# Issue: CocoaPods not installed (Mac)
sudo gem install cocoapods
pod setup
# Issue: Chrome not found (for web development)
# Install Chrome or set CHROME_EXECUTABLE
export CHROME_EXECUTABLE=/usr/bin/google-chrome-stable
# Issue: Visual Studio not installed (Windows desktop)
# Install Visual Studio 2022 with "Desktop development with C++" workload
# Issue: Xcode installation is incomplete
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
Creating Your First Flutter App
# Create a new project
flutter create my_first_app
cd my_first_app
# Run on connected device or emulator
flutter run
# Run on Chrome (web)
flutter run -d chrome
# Run on specific device
flutter devices # List available devices
flutter run -d emulator-5554 # Run on specific device
The Widget Tree Explained
Everything in Flutter is a widget. The text you see, the padding around it, the row it sits in, the button that wraps it — all widgets. Flutter builds the UI by composing widgets into a tree hierarchy, similar to how HTML uses nested elements.
The key difference from web development: in Flutter, layout, styling, and behavior are all defined in Dart code rather than separate HTML, CSS, and JS files. This means your UI is always in sync with your logic.
StatelessWidget vs StatefulWidget
This is the most fundamental concept in Flutter. Understanding when to use each determines the performance and architecture of your app.
StatelessWidget — Immutable UI
Use when the widget's appearance does not change after it is built. It takes input data and renders it — that is all.
class ProfileCard extends StatelessWidget {
final String name;
final String email;
final String avatarUrl;
const ProfileCard({
super.key,
required this.name,
required this.email,
required this.avatarUrl,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(avatarUrl),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(email, style: TextStyle(
color: Colors.grey[600], fontSize: 14)),
],
),
],
),
),
);
}
}
StatefulWidget — Mutable UI
Use when the widget needs to rebuild in response to user interaction, timers, or data changes. The widget owns a State object that persists across rebuilds.
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++;
});
}
void _decrement() {
setState(() {
if (_count > 0) _count--;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_count',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _decrement,
icon: const Icon(Icons.remove),
label: const Text('Decrease'),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: _increment,
icon: const Icon(Icons.add),
label: const Text('Increase'),
),
],
),
],
);
}
}
Navigation and Routing with GoRouter
GoRouter is the recommended routing package for Flutter. It provides declarative routing, deep linking, and URL-based navigation that works across mobile and web.
# Add GoRouter dependency
flutter pub add go_router
// lib/router.dart
import 'package:go_router/go_router.dart';
import 'screens/home_screen.dart';
import 'screens/profile_screen.dart';
import 'screens/settings_screen.dart';
import 'screens/login_screen.dart';
final GoRouter router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = AuthService.instance.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/';
return null; // No redirect
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'profile/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
return ProfileScreen(userId: userId);
},
),
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
errorBuilder: (context, state) => const NotFoundScreen(),
);
// Usage in MaterialApp
MaterialApp.router(
routerConfig: router,
title: 'DreamWebCrafts App',
theme: ThemeData(useMaterial3: true),
);
State Management: Provider, Riverpod, and BLoC
State management is the most debated topic in Flutter. Here is a practical comparison with working examples of each approach.
Provider (Simple and Official)
flutter pub add provider
// Provider example - Shopping Cart
class CartProvider extends ChangeNotifier {
final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price * item.quantity);
void addItem(Product product) {
final existingIndex = _items.indexWhere((item) => item.productId == product.id);
if (existingIndex >= 0) {
_items[existingIndex].quantity++;
} else {
_items.add(CartItem(productId: product.id, name: product.name,
price: product.price, quantity: 1));
}
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.productId == productId);
notifyListeners();
}
}
// Wrap your app with the provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartProvider(),
child: const MyApp(),
),
);
}
// Consume in any widget
class CartBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartProvider>();
return Badge(
label: Text('${cart.itemCount}'),
child: const Icon(Icons.shopping_cart),
);
}
}
Riverpod (Provider 2.0, Compile-Safe)
flutter pub add flutter_riverpod
// Riverpod example - Fetch users from API
final usersProvider = FutureProvider<List<User>>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode != 200) throw Exception('Failed to load users');
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
});
// Consume in widget
class UsersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => ListTile(
title: Text(users[index].name),
subtitle: Text(users[index].email),
),
),
);
}
}
BLoC (Business Logic Component — Enterprise Grade)
flutter pub add flutter_bloc
// BLoC example - Authentication
// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested(this.email, this.password);
}
class LogoutRequested extends AuthEvent {}
// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _authRepository;
AuthBloc(this._authRepository) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<LogoutRequested>(_onLogoutRequested);
}
Future<void> _onLoginRequested(
LoginRequested event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final user = await _authRepository.login(event.email, event.password);
emit(AuthAuthenticated(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event, Emitter<AuthState> emit) async {
await _authRepository.logout();
emit(AuthInitial());
}
}
HTTP Requests with Dio
flutter pub add dio
// lib/services/api_service.dart
import 'package:dio/dio.dart';
class ApiService {
late final Dio _dio;
ApiService() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.dreamwebcrafts.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
// Add interceptors for auth and logging
_dio.interceptors.addAll([
InterceptorsWrapper(
onRequest: (options, handler) {
final token = AuthService.instance.token;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
AuthService.instance.logout();
}
handler.next(error);
},
),
LogInterceptor(requestBody: true, responseBody: true),
]);
}
Future<List<Project>> getProjects() async {
try {
final response = await _dio.get('/projects');
return (response.data as List)
.map((json) => Project.fromJson(json))
.toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
Future<Project> createProject(Map<String, dynamic> data) async {
try {
final response = await _dio.post('/projects', data: data);
return Project.fromJson(response.data);
} on DioException catch (e) {
throw _handleError(e);
}
}
String _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return 'Connection timed out. Check your internet connection.';
case DioExceptionType.receiveTimeout:
return 'Server took too long to respond.';
case DioExceptionType.badResponse:
return e.response?.data['message'] ?? 'Server error';
default:
return 'Network error. Please try again.';
}
}
}
Local Storage: shared_preferences and Hive
# For simple key-value storage
flutter pub add shared_preferences
# For structured local database
flutter pub add hive hive_flutter
flutter pub add --dev hive_generator build_runner
// shared_preferences example
import 'package:shared_preferences/shared_preferences.dart';
class SettingsService {
Future<void> setDarkMode(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('dark_mode', enabled);
}
Future<bool> getDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('dark_mode') ?? false;
}
}
// Hive example - Offline-first data
@HiveType(typeId: 0)
class Task extends HiveObject {
@HiveField(0)
late String title;
@HiveField(1)
late bool isCompleted;
@HiveField(2)
late DateTime createdAt;
}
// Initialize Hive in main.dart
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(TaskAdapter());
await Hive.openBox<Task>('tasks');
runApp(const MyApp());
}
// CRUD operations
final taskBox = Hive.box<Task>('tasks');
// Create
final task = Task()
..title = 'Build Flutter app'
..isCompleted = false
..createdAt = DateTime.now();
await taskBox.add(task);
// Read all
final allTasks = taskBox.values.toList();
// Update
task.isCompleted = true;
await task.save();
// Delete
await task.delete();
Firebase Integration: Auth + Firestore
# Install Firebase CLI and FlutterFire
npm install -g firebase-tools
dart pub global activate flutterfire_cli
# Configure Firebase for your Flutter project
firebase login
flutterfire configure
# Add Firebase packages
flutter pub add firebase_core firebase_auth cloud_firestore
// Firebase Auth - Email/Password Sign In
class FirebaseAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<User?> get authStateChanges => _auth.authStateChanges();
Future<UserCredential> signUp(String email, String password) async {
try {
return await _auth.createUserWithEmailAndPassword(
email: email, password: password);
} on FirebaseAuthException catch (e) {
switch (e.code) {
case 'weak-password':
throw 'Password must be at least 6 characters';
case 'email-already-in-use':
throw 'An account already exists with this email';
default:
throw 'Registration failed: ${e.message}';
}
}
}
Future<UserCredential> signIn(String email, String password) async {
try {
return await _auth.signInWithEmailAndPassword(
email: email, password: password);
} on FirebaseAuthException catch (e) {
switch (e.code) {
case 'user-not-found':
throw 'No account found with this email';
case 'wrong-password':
throw 'Incorrect password';
default:
throw 'Login failed: ${e.message}';
}
}
}
Future<void> signOut() => _auth.signOut();
}
// Firestore CRUD operations
class ProjectRepository {
final _collection = FirebaseFirestore.instance.collection('projects');
Stream<List<Project>> getProjects(String userId) {
return _collection
.where('ownerId', isEqualTo: userId)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => Project.fromFirestore(doc))
.toList());
}
Future<void> addProject(Project project) async {
await _collection.add(project.toMap());
}
Future<void> updateProject(String id, Map<String, dynamic> data) async {
await _collection.doc(id).update(data);
}
Future<void> deleteProject(String id) async {
await _collection.doc(id).delete();
}
}
Building and Signing APK — Step by Step
This is where many beginners get stuck. Follow these steps exactly to build a release APK that you can upload to the Google Play Store.
Step 1: Generate a Keystore
# Generate a keystore file (keep this safe — you cannot recover it)
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA
-keysize 2048 -validity 10000 -alias upload
# You will be prompted for:
# - Keystore password
# - Your name, organization, city, state, country
# - Key password (press Enter to use same as keystore)
Step 2: Create key.properties
# android/key.properties (DO NOT commit this to Git!)
storePassword=your_keystore_password
keyPassword=your_key_password
keyAlias=upload
storeFile=/Users/yourname/upload-keystore.jks
Step 3: Configure build.gradle
// android/app/build.gradle
// Add before the android {} block:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// ... existing config
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Step 4: Build the APK or App Bundle
# Build APK (for direct distribution)
flutter build apk --release
# Build App Bundle (required for Play Store)
flutter build appbundle --release
# Build split APKs (smaller per-device downloads)
flutter build apk --split-per-abi
# Output locations:
# APK: build/app/outputs/flutter-apk/app-release.apk
# Bundle: build/app/outputs/bundle/release/app-release.aab
Building iOS IPA
# Requires a Mac with Xcode installed
# Open iOS project in Xcode
open ios/Runner.xcworkspace
# Set your Team in Xcode:
# Runner > Signing & Capabilities > Team > Select your Apple Developer account
# Build for release
flutter build ipa --release
# The IPA will be at: build/ios/ipa/
# Upload to App Store Connect using Transporter or xcrun
xcrun altool --upload-app -f build/ios/ipa/Runner.ipa
-t ios -u your@apple.id -p @keychain:altool
Common Gradle and CocoaPods Errors
Problem: "Execution failed for task ':app:processDebugMainManifest'"
Cause: Conflicting minimum SDK version between your app and a dependency.
Solution: Set the minSdkVersion in android/app/build.gradle to at least 21 (or whatever the plugin requires). Check the error message for the specific version needed.
android {
defaultConfig {
minSdkVersion 21 // Increase if a plugin requires higher
targetSdkVersion 34
}
}
Problem: "CocoaPods could not find compatible versions"
Cause: Stale CocoaPods cache or outdated Podfile.lock.
Solution:
cd ios
rm -rf Pods Podfile.lock
pod repo update
pod install --repo-update
cd ..
flutter clean
flutter run
Problem: "AAPT: error: resource not found"
Cause: Missing or misconfigured Android resources, often after changing the app icon or splash screen.
Solution: Clean the build and regenerate resources:
flutter clean
cd android
./gradlew clean
cd ..
flutter pub get
flutter run
Problem: "Gradle task assembleDebug failed with exit code 1"
Cause: This is a generic error. The actual cause is in the Gradle output above this line.
Solution: Run with verbose output to find the real error:
flutter run -v 2>&1 | tail -50
Responsive Design in Flutter
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1024) {
return desktop;
} else if (constraints.maxWidth >= 600) {
return tablet ?? mobile;
} else {
return mobile;
}
},
);
}
}
// Usage
ResponsiveLayout(
mobile: const MobileHomeScreen(),
tablet: const TabletHomeScreen(),
desktop: const DesktopHomeScreen(),
)
Play Store Publishing Checklist
- App icon: 512x512 PNG with no transparency
- Feature graphic: 1024x500 PNG
- At least 2 screenshots per supported device type
- Short description (80 chars) and full description (4000 chars)
- Privacy policy URL (mandatory)
- Content rating questionnaire completed
- Target audience and content declarations
- Signed App Bundle (.aab) uploaded
- Select countries for distribution
- Set pricing (free or paid)
- Complete Data Safety form
Quick Reference: Flutter CLI Cheat Sheet
| Command | Purpose |
|---|---|
flutter create app_name | Create new project |
flutter run | Run on connected device |
flutter run -d chrome | Run on web browser |
flutter pub add package_name | Add a dependency |
flutter pub get | Install dependencies |
flutter clean | Clear build cache |
flutter doctor -v | Diagnose environment |
flutter build apk --release | Build release APK |
flutter build appbundle | Build Play Store bundle |
flutter build ipa | Build iOS archive |
flutter test | Run unit tests |
flutter analyze | Static code analysis |
dart fix --apply | Auto-fix lint issues |
flutter devices | List connected devices |
Flutter has matured into a production-ready framework trusted by companies like Google, BMW, and Alibaba. Whether you are building a startup MVP or an enterprise application, Flutter delivers native performance with a single codebase. At DreamWebCrafts, we build Flutter applications for clients who need to launch on both iOS and Android simultaneously without doubling their development budget. Contact us to discuss your mobile app project.