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

  1. App icon: 512x512 PNG with no transparency
  2. Feature graphic: 1024x500 PNG
  3. At least 2 screenshots per supported device type
  4. Short description (80 chars) and full description (4000 chars)
  5. Privacy policy URL (mandatory)
  6. Content rating questionnaire completed
  7. Target audience and content declarations
  8. Signed App Bundle (.aab) uploaded
  9. Select countries for distribution
  10. Set pricing (free or paid)
  11. Complete Data Safety form

Quick Reference: Flutter CLI Cheat Sheet

Command Purpose
flutter create app_nameCreate new project
flutter runRun on connected device
flutter run -d chromeRun on web browser
flutter pub add package_nameAdd a dependency
flutter pub getInstall dependencies
flutter cleanClear build cache
flutter doctor -vDiagnose environment
flutter build apk --releaseBuild release APK
flutter build appbundleBuild Play Store bundle
flutter build ipaBuild iOS archive
flutter testRun unit tests
flutter analyzeStatic code analysis
dart fix --applyAuto-fix lint issues
flutter devicesList 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.