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.