Toolmingo
Guides18 min read

Flutter Developer Roadmap 2025 (Step-by-Step Guide)

The complete Flutter developer roadmap for 2025 — from Dart basics through widgets, state management, Firebase, testing, and shipping to the App Store and Play Store.

Flutter is Google's open-source UI toolkit for building natively compiled apps for mobile, web, and desktop from a single codebase. Over 1 million apps on the Play Store are built with Flutter. This roadmap shows exactly what to learn, in what order, and what to build along the way.

At a glance

Phase Topic Timeline
1 Dart language fundamentals Weeks 1–3
2 Flutter basics & widgets Weeks 4–7
3 Layouts & responsive design Weeks 8–10
4 Navigation & routing Weeks 11–12
5 State management Weeks 13–16
6 HTTP & API integration Weeks 17–18
7 Local storage Weeks 19–20
8 Firebase & backend Weeks 21–23
9 Testing Weeks 24–26
10 Publishing & DevOps Weeks 27–30
11 Advanced topics Weeks 31–36

Phase 1 — Dart language fundamentals (Weeks 1–3)

Flutter apps are written in Dart. You must be comfortable with Dart before touching Flutter.

Variables and types

// Type inference
var name = 'Flutter';       // String
var count = 42;             // int
var price = 9.99;           // double
var isReady = true;         // bool

// Explicit types (preferred in production)
String greeting = 'Hello';
int age = 25;
List<String> tags = ['flutter', 'dart', 'mobile'];
Map<String, dynamic> user = {'name': 'Ana', 'age': 30};

// Constants
const pi = 3.14159;         // compile-time constant
final now = DateTime.now(); // runtime constant (set once)

Null safety

// Non-nullable (default in Dart 3)
String name = 'Ana';           // cannot be null

// Nullable — add ?
String? nickname;              // can be null
print(nickname ?? 'No nickname'); // null-aware operator

// Late variables — non-null but initialized later
late String token;
token = fetchToken();

// Null assertion
String definitelyNotNull = nickname!; // throws if null

Functions

// Regular function
int add(int a, int b) => a + b;

// Named parameters (most Flutter widgets use these)
void greet({required String name, String? title}) {
  print('Hello, ${title ?? ''} $name');
}
greet(name: 'Ana', title: 'Dr.');

// Optional positional
String repeat(String s, [int times = 2]) => s * times;

// Higher-order functions
List<int> doubled = [1, 2, 3].map((n) => n * 2).toList();
List<int> evens = [1, 2, 3, 4].where((n) => n.isEven).toList();

Classes and OOP

class Animal {
  final String name;
  int _age; // private field

  Animal(this.name, this._age);

  // Getter
  int get age => _age;

  // Method
  String speak() => '$name makes a sound';

  // Factory constructor
  factory Animal.fromJson(Map<String, dynamic> json) {
    return Animal(json['name'], json['age']);
  }
}

class Dog extends Animal {
  Dog(String name, int age) : super(name, age);

  @override
  String speak() => '$name barks!';
}

// Mixins
mixin Swimmer {
  void swim() => print('swimming');
}

class Duck extends Animal with Swimmer {
  Duck() : super('Duck', 1);
}

Async programming

// Future — single async value
Future<String> fetchUser(int id) async {
  final response = await http.get(Uri.parse('/users/$id'));
  return response.body;
}

// Stream — sequence of async values
Stream<int> countdown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

// Error handling
try {
  final user = await fetchUser(1);
} on SocketException {
  print('No internet connection');
} catch (e) {
  print('Error: $e');
}
Dart concept Flutter use case
Null safety Widget properties, API responses
Named parameters Widget constructors (Text('Hi', style: ...))
async/await HTTP calls, database queries, Firebase
Streams Real-time data, Firebase snapshots
Generics List<Widget>, Future<User>, state types

Checkpoint: Solve 20 Dart exercises on DartPad before writing your first Flutter app.


Phase 2 — Flutter basics & widgets (Weeks 4–7)

Everything is a widget

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(colorSchemeSeed: Colors.blue),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: const Center(child: Text('Hello Flutter!')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

Stateless vs StatefulWidget

// StatelessWidget — no mutable state
class Greeting extends StatelessWidget {
  final String name;
  const Greeting({super.key, required this.name});

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name!');
  }
}

// StatefulWidget — mutable state with setState
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  void _increment() {
    setState(() => _count++);  // triggers rebuild
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(onPressed: _increment, child: const Text('+')),
      ],
    );
  }
}

Common widgets cheat sheet

Widget Purpose Key properties
Text Display text style, textAlign, overflow, maxLines
Image Display image Image.asset(), Image.network(), fit
Icon Material icon Icons.home, size, color
ElevatedButton Raised button onPressed, child, style
TextButton Flat button onPressed, child
TextField Text input controller, onChanged, decoration
Checkbox Boolean toggle value, onChanged
Switch Toggle switch value, onChanged
Slider Range picker value, min, max, onChanged
CircularProgressIndicator Loading spinner value (null = indeterminate)
SnackBar Toast message content, action
AlertDialog Popup dialog title, content, actions
BottomSheet Slide-up panel showModalBottomSheet()

Phase 3 — Layouts & responsive design (Weeks 8–10)

Layout widgets

// Column — vertical arrangement
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    const Text('First'),
    const SizedBox(height: 8),  // spacer
    const Text('Second'),
  ],
)

// Row — horizontal arrangement
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    const Icon(Icons.person),
    const Expanded(child: Text('Name')),  // fills remaining space
    const Text('Details'),
  ],
)

// Stack — overlapping widgets
Stack(
  alignment: Alignment.center,
  children: [
    Container(width: 200, height: 200, color: Colors.blue),
    const Text('Overlaid text'),
    Positioned(bottom: 8, right: 8, child: Icon(Icons.star)),
  ],
)

// GridView
GridView.count(
  crossAxisCount: 2,
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  children: items.map((item) => ItemCard(item: item)).toList(),
)

// ListView
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(
    title: Text(items[index].name),
    subtitle: Text(items[index].desc),
    trailing: const Icon(Icons.arrow_forward_ios),
    onTap: () {},
  ),
)

Responsive design

// LayoutBuilder — react to parent constraints
LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth > 600) {
      return DesktopLayout();
    }
    return MobileLayout();
  },
)

// MediaQuery — screen size & orientation
final size = MediaQuery.of(context).size;
final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;

// Flexible & Expanded
Row(
  children: [
    Expanded(flex: 2, child: LeftPanel()),   // 2/3 of width
    Expanded(flex: 1, child: RightPanel()),  // 1/3 of width
  ],
)

Container decoration

Container(
  width: 200,
  height: 100,
  margin: const EdgeInsets.all(16),
  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 8,
        offset: const Offset(0, 2),
      ),
    ],
    gradient: const LinearGradient(
      colors: [Colors.blue, Colors.purple],
    ),
  ),
  child: const Text('Styled box'),
)

Phase 4 — Navigation & routing (Weeks 11–12)

Navigator 1.0 (push/pop)

// Navigate to a new screen
Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => DetailPage(id: item.id)),
);

// Navigate and remove all previous routes (e.g., after login)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (_) => HomePage()),
  (route) => false,
);

// Go back and return a result
Navigator.pop(context, 'result');

// Receive result
final result = await Navigator.push<String>(
  context,
  MaterialPageRoute(builder: (_) => InputPage()),
);

GoRouter (recommended for production)

// pubspec.yaml: go_router: ^14.0.0

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(path: '/', builder: (_, __) => const HomePage()),
    GoRoute(
      path: '/product/:id',
      builder: (_, state) => ProductPage(id: state.pathParameters['id']!),
    ),
    GoRoute(path: '/cart', builder: (_, __) => const CartPage()),
    ShellRoute(
      builder: (_, __, child) => ScaffoldWithNav(child: child),
      routes: [
        GoRoute(path: '/home', builder: (_, __) => const HomeTab()),
        GoRoute(path: '/search', builder: (_, __) => const SearchTab()),
        GoRoute(path: '/profile', builder: (_, __) => const ProfileTab()),
      ],
    ),
  ],
);

// Navigate
context.go('/product/42');
context.push('/cart');
context.pop();

Phase 5 — State management (Weeks 13–16)

State management is the most debated topic in Flutter. Here is what each option is good for:

Solution Best for Learning curve
setState Local widget state (forms, toggles) Beginner
InheritedWidget / Provider App-wide state, dependency injection Intermediate
Riverpod Complex apps, testability, type safety Intermediate
Bloc / Cubit Large teams, strict separation of concerns Advanced
Zustand (flutter_zustand) Small global stores Beginner
GetX Rapid prototyping Beginner (controversial)

Provider

// pubspec.yaml: provider: ^6.1.0

// 1. Create a ChangeNotifier
class CartProvider extends ChangeNotifier {
  final List<CartItem> _items = [];
  List<CartItem> get items => List.unmodifiable(_items);
  int get total => _items.fold(0, (sum, item) => sum + item.price);

  void add(CartItem item) {
    _items.add(item);
    notifyListeners();  // triggers rebuild
  }

  void remove(CartItem item) {
    _items.remove(item);
    notifyListeners();
  }
}

// 2. Provide at root
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CartProvider()),
    ChangeNotifierProvider(create: (_) => AuthProvider()),
  ],
  child: const MyApp(),
)

// 3. Consume
class CartBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = context.watch<CartProvider>();
    return Text('${cart.items.length} items');
  }
}

// Read without rebuilding
context.read<CartProvider>().add(item);

Riverpod (recommended for new projects)

// pubspec.yaml: flutter_riverpod: ^2.5.0

// 1. Wrap app
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

// 2. Define providers
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}

// Async provider (replaces FutureBuilder)
final usersProvider = FutureProvider<List<User>>((ref) async {
  return ref.watch(userRepositoryProvider).fetchAll();
});

// 3. Consume with ConsumerWidget
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return ElevatedButton(
      onPressed: () => ref.read(counterProvider.notifier).increment(),
      child: Text('Count: $count'),
    );
  }
}

Phase 6 — HTTP & API integration (Weeks 17–18)

// pubspec.yaml: http: ^1.2.0 OR dio: ^5.4.0

// http package
import 'package:http/http.dart' as http;
import 'dart:convert';

class UserRepository {
  static const _base = 'https://api.example.com';

  Future<List<User>> fetchAll() async {
    final response = await http.get(
      Uri.parse('$_base/users'),
      headers: {'Authorization': 'Bearer $token'},
    );

    if (response.statusCode != 200) {
      throw Exception('Failed: ${response.statusCode}');
    }

    final List json = jsonDecode(response.body);
    return json.map(User.fromJson).toList();
  }

  Future<User> create(CreateUserDto dto) async {
    final response = await http.post(
      Uri.parse('$_base/users'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(dto.toJson()),
    );
    return User.fromJson(jsonDecode(response.body));
  }
}

// Model with fromJson
class User {
  final int id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(id: json['id'], name: json['name'], email: json['email']);
  }

  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
}

// FutureBuilder — display async data
FutureBuilder<List<User>>(
  future: userRepository.fetchAll(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }
    return ListView(
      children: snapshot.data!.map((u) => Text(u.name)).toList(),
    );
  },
)

Phase 7 — Local storage (Weeks 19–20)

Package Use case Storage type
shared_preferences Simple key-value (settings, flags) Disk (key-value)
sqflite Relational data (offline DB) SQLite
hive Fast NoSQL, works on all platforms Binary file
isar Fast cross-platform NoSQL Custom binary
flutter_secure_storage Sensitive data (tokens, passwords) Keychain/Keystore
path_provider File paths for documents/cache OS filesystem
// shared_preferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', jwtToken);
final token = prefs.getString('token');
await prefs.remove('token');

// flutter_secure_storage (tokens, passwords)
const storage = FlutterSecureStorage();
await storage.write(key: 'jwt', value: token);
final jwt = await storage.read(key: 'jwt');

// sqflite — SQLite
final db = await openDatabase(
  join(await getDatabasesPath(), 'app.db'),
  onCreate: (db, version) async {
    await db.execute('''
      CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT NOT NULL
      )
    ''');
  },
  version: 1,
);

// Insert
await db.insert('users', user.toMap());

// Query
final List<Map<String, dynamic>> maps = await db.query('users');
final users = maps.map(User.fromMap).toList();

// Hive
Hive.registerAdapter(UserAdapter());
await Hive.initFlutter();
final box = await Hive.openBox<User>('users');
await box.put('user_1', user);
final user = box.get('user_1');

Phase 8 — Firebase & backend (Weeks 21–23)

Firebase is the most popular backend choice for Flutter apps.

# pubspec.yaml
dependencies:
  firebase_core: ^3.0.0
  firebase_auth: ^5.0.0
  cloud_firestore: ^5.0.0
  firebase_storage: ^12.0.0
  firebase_messaging: ^15.0.0
// Initialize (main.dart)
await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);

// Authentication
final auth = FirebaseAuth.instance;

// Email/password sign up
final credential = await auth.createUserWithEmailAndPassword(
  email: email,
  password: password,
);

// Sign in
await auth.signInWithEmailAndPassword(email: email, password: password);

// Google sign in (requires google_sign_in package)
final googleUser = await GoogleSignIn().signIn();
final googleAuth = await googleUser!.authentication;
final oauthCredential = GoogleAuthProvider.credential(
  accessToken: googleAuth.accessToken,
  idToken: googleAuth.idToken,
);
await auth.signInWithCredential(oauthCredential);

// Auth state stream
StreamBuilder<User?>(
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    if (snapshot.hasData) return const HomePage();
    return const LoginPage();
  },
)

// Firestore CRUD
final db = FirebaseFirestore.instance;

// Create
await db.collection('posts').add({
  'title': title,
  'body': body,
  'createdAt': FieldValue.serverTimestamp(),
  'uid': auth.currentUser!.uid,
});

// Real-time listener
StreamBuilder<QuerySnapshot>(
  stream: db.collection('posts').orderBy('createdAt', descending: true).snapshots(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const CircularProgressIndicator();
    final docs = snapshot.data!.docs;
    return ListView(
      children: docs.map((doc) {
        final data = doc.data() as Map<String, dynamic>;
        return Text(data['title']);
      }).toList(),
    );
  },
)
Firebase service Flutter package Use case
Authentication firebase_auth Sign in, sign up, Google/Apple
Firestore cloud_firestore Real-time NoSQL database
Storage firebase_storage Images, files, videos
Messaging (FCM) firebase_messaging Push notifications
Analytics firebase_analytics User events, funnels
Crashlytics firebase_crashlytics Crash reporting
Remote Config firebase_remote_config Feature flags, A/B testing

Phase 9 — Testing (Weeks 24–26)

// Unit test
import 'package:test/test.dart';

void main() {
  group('CartProvider', () {
    test('adds item and updates total', () {
      final cart = CartProvider();
      cart.add(CartItem(name: 'Shirt', price: 29));
      expect(cart.items.length, equals(1));
      expect(cart.total, equals(29));
    });

    test('removing item decrements total', () {
      final cart = CartProvider();
      final item = CartItem(name: 'Shoes', price: 89);
      cart.add(item);
      cart.remove(item);
      expect(cart.items, isEmpty);
    });
  });
}

// Widget test
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Counter increments on button tap', (tester) async {
    await tester.pumpWidget(const MyApp());

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();  // trigger rebuild

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });

  testWidgets('Shows error when API fails', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [usersProvider.overrideWith((ref) => throw Exception('Network error'))],
        child: const MyApp(),
      ),
    );
    await tester.pump();
    expect(find.text('Network error'), findsOneWidget);
  });
}

// Integration test (runs on real device/emulator)
// test_driver/app_test.dart
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Login flow', (tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.enterText(find.byKey(Key('email')), 'user@test.com');
    await tester.enterText(find.byKey(Key('password')), 'secret');
    await tester.tap(find.text('Login'));
    await tester.pumpAndSettle();
    expect(find.text('Welcome'), findsOneWidget);
  });
}
Test type Command Speed What it tests
Unit flutter test Fast Business logic, models
Widget flutter test Medium UI rendering, interactions
Integration flutter test integration_test/ Slow Full app on device/emulator
Golden flutter test --update-goldens Medium Pixel-perfect UI screenshots

Phase 10 — Publishing & DevOps (Weeks 27–30)

Android publishing

# Generate upload keystore (one-time)
keytool -genkey -v -keystore upload-keystore.jks \
  -keyalg RSA -keysize 2048 -validity 10000 -alias upload

# android/key.properties
storePassword=<password>
keyPassword=<password>
keyAlias=upload
storeFile=../upload-keystore.jks

# Build release APK / AAB
flutter build apk --release
flutter build appbundle --release  # preferred for Play Store

iOS publishing

# Requires macOS + Xcode + Apple Developer account ($99/year)
flutter build ipa --release
# Upload via Xcode Organizer or Transporter app

CI/CD with GitHub Actions

# .github/workflows/flutter.yml
name: Flutter CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.x'
          channel: 'stable'
      - run: flutter pub get
      - run: flutter analyze
      - run: flutter test --coverage
      - uses: codecov/codecov-action@v4

  build-android:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter build appbundle --release
      - uses: actions/upload-artifact@v4
        with:
          name: app-release
          path: build/app/outputs/bundle/release/

Useful Flutter CLI commands

flutter create my_app                    # new project
flutter pub get                          # install dependencies
flutter pub add riverpod                 # add package
flutter run                              # run on connected device
flutter run -d chrome                    # run on web
flutter build apk --release              # build Android APK
flutter build appbundle --release        # build Android App Bundle
flutter build ipa --release              # build iOS IPA
flutter analyze                          # static analysis
flutter test                             # run all tests
flutter clean                            # clean build cache
flutter upgrade                          # upgrade Flutter SDK
flutter doctor                           # check environment setup

Phase 11 — Advanced topics (Weeks 31–36)

Animations

// Implicit animation (simplest)
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: _expanded ? 200 : 100,
  height: _expanded ? 200 : 100,
  color: _expanded ? Colors.blue : Colors.red,
)

// Hero animation (shared element transition)
// On source page:
Hero(tag: 'product-${product.id}', child: Image.network(product.imageUrl))

// On destination page (same tag):
Hero(tag: 'product-${product.id}', child: Image.network(product.imageUrl))

// Explicit animation with AnimationController
class FadeInWidget extends StatefulWidget { ... }
class _FadeInWidgetState extends State<FadeInWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
    _opacity = Tween<double>(begin: 0, end: 1).animate(_controller);
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(opacity: _opacity, child: widget.child);
  }
}

Platform channels (native code)

// Dart side — call native code
static const _channel = MethodChannel('com.myapp/battery');

Future<int> getBatteryLevel() async {
  return await _channel.invokeMethod<int>('getBatteryLevel') ?? -1;
}

// Android side (Kotlin — MainActivity.kt)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.myapp/battery")
  .setMethodCallHandler { call, result ->
    if (call.method == "getBatteryLevel") {
      result.success(getBatteryLevel())
    } else {
      result.notImplemented()
    }
  }

Performance tips

Issue Cause Fix
Jank / dropped frames Expensive work on main thread Move to compute() or isolates
Unnecessary rebuilds setState too high in tree const constructors, selective watch
Large list performance Building all items at once ListView.builder (lazy)
Image loading lag Full-size images cached_network_image, resize
App size too large Debug build artifacts flutter build --release --split-per-abi

Realistic timeline (36 weeks)

Weeks Milestone What you can build
1–3 Dart comfortable CLI tools, algorithms
4–7 Flutter basics Static screens, multi-screen app
8–10 Responsive layouts Adaptive UI for phone + tablet
11–12 Navigation App with 4–5 screens, bottom nav
13–16 State management To-do app, shopping cart
17–18 HTTP + APIs News app, weather app
19–20 Local storage Offline note app, cached data
21–23 Firebase Auth + real-time chat / CRUD
24–26 Testing 80%+ coverage on one project
27–30 Published App live on Play Store
31–36 Advanced Animations, platform channels, CI/CD

Portfolio project ideas

Project Flutter concepts Difficulty
To-Do App setState, CRUD, local storage ⭐⭐
Weather App HTTP, JSON, FutureBuilder, location ⭐⭐
Chat App Firebase Auth, Firestore, StreamBuilder ⭐⭐⭐
E-commerce App Riverpod, navigation, Stripe integration ⭐⭐⭐
Fitness Tracker sqflite, charts, HealthKit/Health Connect ⭐⭐⭐
News Reader REST API, infinite scroll, WebView ⭐⭐⭐
Maps App Google Maps, location services, geofencing ⭐⭐⭐⭐
Video Player App Platform channels, custom controls, HLS ⭐⭐⭐⭐

Flutter developer roles and salary

Role Level US salary EU salary
Junior Flutter Developer 0–2 yr $55–75k €28–42k
Mid-level Flutter Developer 2–5 yr $80–110k €48–72k
Senior Flutter Developer 5+ yr $115–155k €72–105k
Flutter / Dart Full-Stack 3+ yr $95–135k €60–90k
Mobile Lead (Flutter) 5+ yr $125–165k €80–115k
Flutter Consultant / Freelance 3+ yr $75–140/hr €55–110/hr

Common mistakes table

Mistake Why it hurts Fix
setState everywhere Rebuilds whole tree, hard to scale Use Riverpod/Provider for shared state
setState inside async without mounted check Crash after widget is disposed if (mounted) setState(...)
Not using const constructors Unnecessary rebuilds Add const wherever possible
Missing dispose() Memory leaks (controllers, streams) Always dispose AnimationController, TextEditingController, etc.
Putting business logic in widgets Hard to test, messy Extract to providers/notifiers/repositories
Using print() in production Leaks data, performance hit Use debugPrint or a logging package
Ignoring flutter analyze warnings Silent bugs accumulate Run flutter analyze in CI
Targeting only one platform early Surprises on iOS when developed on Android Test on both platforms from day one

Flutter vs React Native vs Kotlin Multiplatform

Flutter React Native Kotlin Multiplatform
Language Dart JavaScript / TypeScript Kotlin
UI rendering Own engine (Skia / Impeller) Native components via bridge Native UI per platform
Performance Near-native Good (New Architecture) Native
Code sharing 95–100% across platforms 70–95% 60–80% (logic only)
Supported targets iOS, Android, Web, Desktop iOS, Android, Web iOS, Android, Web, Desktop
Hot reload Yes Yes No
Community Large, Google-backed Large, Meta-backed Growing, JetBrains-backed
Best for Cross-platform apps with custom UI Teams with web/JS background Teams with Kotlin/Android background
Corporate use Google Pay, BMW, Alibaba Facebook, Instagram, Shopify Netflix, Philips, VMware

6 frequently asked questions

Q: Do I need to learn Android or iOS development first?
A: No. Flutter's value proposition is exactly that you skip platform-specific development. Learn Dart + Flutter directly. You will need a Mac to publish iOS apps (Xcode requirement), but you can develop and test on an Android emulator on any OS.

Q: Which state management should I start with?
A: Start with setState to understand reactivity. Then move to Riverpod for anything beyond a single widget. Riverpod is the community favourite in 2025 — it is testable, type-safe, and avoids most of Provider's footguns. Avoid GetX — it mixes too many concerns and makes testing harder.

Q: Is Flutter good for production apps?
A: Yes. Google Pay, BMW app, eBay Motors, Alibaba's Xianyu, and thousands of other production apps are built with Flutter. The ecosystem is mature, and Flutter 3.x supports iOS, Android, Web, macOS, Windows, and Linux from a single codebase.

Q: Flutter or React Native in 2025 — which should I learn?
A: Flutter has better performance (no JavaScript bridge), more consistent UI across platforms, and faster growth. React Native is better if your team already knows JavaScript and you want to leverage the npm ecosystem. Both are production-ready. Flutter is winning on mobile-first teams; React Native still dominates web-plus-mobile teams.

Q: How long until I can get a Flutter job?
A: A focused learner can be job-ready in 6–9 months (following this roadmap). What matters most to employers: 2–3 apps published on the Play Store, Riverpod or Bloc proficiency, Firebase integration experience, and clean code with tests. Contribute to open-source Flutter packages to accelerate visibility.

Q: Does Flutter work for web and desktop too?
A: Yes, Flutter 3 supports web, macOS, Windows, and Linux. Web support is good for dashboards and internal tools but is not ideal for SEO-heavy public websites (use Next.js or Nuxt for those). Desktop support is production-ready for macOS and Windows. The real strength remains iOS + Android from one codebase.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools