Flutter Developer Roadmap: The Complete Free Guide
Flutter is Google's toolkit for building apps for mobile, web, and desktop from a single codebase. It was released in 2017, and in just a few years it went from a niche experiment to one of the most requested skills in mobile job postings.
The reason is simple. A company that needs an iOS app and an Android app used to need two separate teams writing two separate codebases in two different languages. Flutter lets one team write one codebase in Dart and ship to both platforms, often with pixel identical results. That is a massive cost saving, and it is why companies like Google, Toyota, BMW, Nubank, and CRED all run parts of their production apps on Flutter today.
This roadmap takes you from your first widget to a published, production ready app, using only free resources.
Who Is This Roadmap For?
- Students who want to build mobile apps but do not want to learn Swift and Kotlin separately
- Web developers who want to move into mobile development without starting from zero
- Anyone who wants to ship an app to both the Play Store and the App Store without maintaining two codebases
- Developers who want a framework backed by Google with a large, active job market
You do not need any prior mobile development experience. If you have written code in any language before, Dart will feel familiar within a week.
Why Learn Flutter in the First Place?
Before you commit months to learning a framework, you deserve a straight answer on why Flutter specifically.
One codebase, multiple platforms: Write your UI once in Dart and compile it to native ARM code for iOS, Android, web, Windows, macOS, and Linux. No bridge, no JavaScript interpreter at runtime, just compiled native code.
Everything is a widget: Flutter's entire UI is built from widgets, composed together like building blocks. Once this idea clicks, building complex interfaces becomes a matter of combining small, well understood pieces rather than fighting a framework.
Hot reload: Change your code and see the result in your running app in under a second, without losing app state. This alone can save you hours every single day compared to native development cycles.
Real companies, real jobs: Google Pay's rewrite in Flutter cut its codebase by 35 percent and now runs for hundreds of millions of users in India alone. CRED, Dream11, Kotak Mahindra Bank, Nubank, Toyota, and BMW all ship real production features in Flutter. Mobile teams increasingly list Flutter as a required or preferred skill for app developer roles.
Consistent design across platforms: Because Flutter draws every pixel itself instead of relying on native platform widgets, your app looks and behaves the same on an old Android phone and a new iPhone. That consistency is a big reason design and product teams push for Flutter.
Phase 1: Dart Fundamentals (3 to 4 weeks)
Setting Up Your Environment
- Install the Flutter SDK from flutter.dev, free for all platforms
- Install Android Studio or VS Code with the Flutter and Dart extensions
- Run
flutter doctorin your terminal to verify your setup and fix any missing dependencies - Set up an emulator (Android) or simulator (iOS on macOS) or connect a physical device
Free Resource:
Your First Dart Program
void main() {
print('Hello, World!');
}
Run it with dart run main.dart. Dart is the language, Flutter is the framework built on top of it. You cannot skip Dart and go straight to Flutter, so spend real time here first.
Variables and Types
// Type inferred
var name = 'Rahul';
// Explicit type
String city = 'Mumbai';
int age = 22;
double price = 499.99;
bool isStudent = true;
// Constants
final currentUser = 'Rahul'; // set once, at runtime
const pi = 3.14159; // set once, at compile time
// Null safety, Dart's biggest feature
String? nickname; // can be null
String username = nickname ?? 'Guest'; // fallback if null
print(nickname?.length); // safe access, returns null if nickname is null
Dart is null safe by default. A variable of type String can never be null unless you explicitly mark it String?. This eliminates an entire category of runtime crashes that used to plague mobile apps.
Control Flow
// if / else
if (score >= 90) {
print('A grade');
} else if (score >= 80) {
print('B grade');
} else {
print('Try harder');
}
// switch
switch (day) {
case 'Monday':
case 'Tuesday':
print('Start of week');
break;
default:
print('Other day');
}
// for loops
for (var i = 0; i < 10; i++) {
print(i);
}
for (var item in items) {
print(item);
}
// while
while (count < 100) {
count++;
}
Functions
// Basic function
int add(int a, int b) {
return a + b;
}
// Arrow syntax for single expression functions
int square(int x) => x * x;
// Optional named parameters
void greet({required String name, String greeting = 'Hello'}) {
print('$greeting, $name!');
}
greet(name: 'Priya'); // Hello, Priya!
// Optional positional parameters
void log(String message, [String level = 'INFO']) {
print('[$level] $message');
}
// Functions as values
final multiply = (int a, int b) => a * b;
print(multiply(3, 4)); // 12
Collections
// List
List<String> fruits = ['apple', 'banana', 'mango'];
fruits.add('grape');
fruits.remove('banana');
final firstTwo = fruits.take(2).toList();
// Map
Map<String, int> ages = {'Rahul': 22, 'Priya': 21};
ages['Amit'] = 23;
print(ages['Rahul']);
// Set, unique values only
Set<String> uniqueTags = {'flutter', 'dart', 'flutter'}; // duplicates removed automatically
// Collection operations you will use constantly
final doubled = [1, 2, 3].map((n) => n * 2).toList();
final evens = [1, 2, 3, 4, 5].where((n) => n % 2 == 0).toList();
final total = [1, 2, 3].fold(0, (sum, n) => sum + n);
Classes and Object Oriented Dart
class User {
final String name;
final int age;
User({required this.name, required this.age});
// Named constructor
User.guest() : name = 'Guest', age = 0;
// Method
void introduce() {
print('Hi, I am $name, $age years old');
}
}
// Inheritance
class Admin extends User {
final List<String> permissions;
Admin({required super.name, required super.age, required this.permissions});
@override
void introduce() {
super.introduce();
print('I have admin rights');
}
}
// Abstract classes and interfaces
abstract class Shape {
double area();
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double area() => 3.14159 * radius * radius;
}
Asynchronous Dart
Nearly everything in Flutter that touches the network, a file, or a database is asynchronous. Understand this section thoroughly.
// Future, a value that will be available later
Future<String> fetchUserName() async {
await Future.delayed(Duration(seconds: 2));
return 'Rahul';
}
void loadUser() async {
print('Loading...');
final name = await fetchUserName();
print('Loaded: $name');
}
// Handling errors
Future<void> loadData() async {
try {
final data = await fetchUserName();
print(data);
} catch (e) {
print('Error: $e');
}
}
// Stream, a sequence of async values over time
Stream<int> countStream() async* {
for (var i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void listenToStream() {
countStream().listen((value) {
print('Received: $value');
});
}
Free Resources:
- Dart Language Tour, official, free, the best starting point
- DartPad, run Dart code in your browser instantly, free
- Dart Fundamentals course, freeCodeCamp, free YouTube course
Phase 2: Flutter Fundamentals and Widgets (3 to 4 weeks)
Everything Is a Widget
In Flutter, buttons, padding, text, colors, and even entire screens are all widgets. You build UI by composing widgets inside other widgets.
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 First App',
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}
StatelessWidget vs StatefulWidget
// StatelessWidget, does not change over time
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name');
}
}
// StatefulWidget, holds mutable state
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++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: increment,
child: const Text('Increment'),
),
],
);
}
}
setState tells Flutter that something changed and it needs to rebuild that part of the widget tree. This is the simplest form of state management, and you should understand it before reaching for any external state management library.
Common Widgets You Will Use Every Day
Text('Hello');
Icon(Icons.home);
Image.network('https://example.com/photo.jpg');
Image.asset('assets/logo.png');
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text('Boxed content'),
);
ElevatedButton(onPressed: () {}, child: const Text('Submit'));
TextButton(onPressed: () {}, child: const Text('Cancel'));
IconButton(icon: const Icon(Icons.favorite), onPressed: () {});
TextField(
decoration: const InputDecoration(labelText: 'Email'),
onChanged: (value) => print(value),
);
Widget Lifecycle
class LifecycleDemo extends StatefulWidget {
const LifecycleDemo({super.key});
@override
State<LifecycleDemo> createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State<LifecycleDemo> {
@override
void initState() {
super.initState();
// Called once when the widget is inserted into the tree
// Good place to start API calls, subscriptions
}
@override
void didUpdateWidget(LifecycleDemo oldWidget) {
super.didUpdateWidget(oldWidget);
// Called when the parent rebuilds this widget with new data
}
@override
void dispose() {
// Called when the widget is removed permanently
// Cancel timers, close streams, dispose controllers here
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Free Resources:
- Flutter Widget Catalog, official, browse every widget with examples
- Flutter Crash Course, The Net Ninja, free
- Flutter Cookbook, official, task focused recipes
Phase 3: Layouts and Responsive UI (3 to 4 weeks)
Row, Column, and Stack
// Horizontal layout
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(Icons.star),
Text('Rating: 4.5'),
Icon(Icons.arrow_forward),
],
);
// Vertical layout
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Title', style: TextStyle(fontSize: 20)),
Text('Subtitle'),
],
);
// Overlapping widgets
Stack(
children: [
Image.network('https://example.com/banner.jpg'),
Positioned(
bottom: 10,
right: 10,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.black54,
child: const Text('New', style: TextStyle(color: Colors.white)),
),
),
],
);
Scrollable Lists
// Simple scrollable list
ListView(
children: const [
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
],
);
// Efficient, lazily built list for large data sets
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.person),
title: Text(items[index].name),
subtitle: Text(items[index].email),
onTap: () => print('Tapped ${items[index].name}'),
);
},
);
// Grid layout
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
);
Responsive Design
// LayoutBuilder, react to available space
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return const TwoColumnLayout();
}
return const SingleColumnLayout();
},
);
// MediaQuery, read screen size and device info
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
final padding = MediaQuery.of(context).padding; // safe area insets
// Expanded and Flexible for proportional sizing
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.blue)),
Expanded(flex: 1, child: Container(color: Colors.red)),
],
);
Theming
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
useMaterial3: true,
),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system, // follows device setting automatically
home: const HomeScreen(),
);
Free Resources:
- Flutter Layout Tutorial, official
- Responsive Design in Flutter, official guide
Phase 4: Navigation and Routing (2 to 3 weeks)
Navigator 1.0, Simple Stack Based Navigation
// Push a new screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailsScreen()),
);
// Pop back to previous screen
Navigator.pop(context);
// Pass data forward and get a result back
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectItemScreen()),
);
// Named routes, defined once in MaterialApp
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
},
);
Navigator.pushNamed(context, '/details');
go_router, the Recommended Package for Real Apps
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductScreen(id: id);
},
),
],
redirect: (context, state) {
final loggedIn = AuthService.isLoggedIn;
if (!loggedIn && state.matchedLocation != '/login') {
return '/login';
}
return null;
},
);
MaterialApp.router(routerConfig: router);
go_router supports deep linking, URL based navigation on Flutter web, and nested navigation out of the box. Most production apps use it instead of plain Navigator 1.0.
Free Resources:
- go_router Documentation, free, official package
- Flutter Navigation Guide, official
Phase 5: State Management (4 to 5 weeks)
This is the topic that separates beginner Flutter developers from job ready ones. setState works for a single widget, but real apps need to share state across many screens.
Provider, the Simplest Widely Used Option
import 'package:provider/provider.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => _items;
void add(String item) {
_items.add(item);
notifyListeners();
}
}
// Register at the top of the app
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
);
// Read and rebuild on change
class CartScreen extends StatelessWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Text('Items: ${cart.items.length}');
}
}
// Call a method without rebuilding
context.read<CartModel>().add('Laptop');
Riverpod, the Modern Successor to Provider
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('Increment'),
),
],
);
}
}
Riverpod fixes several design limitations in Provider and works without needing a BuildContext, which makes it easier to test and use outside the widget tree.
Bloc, the Structured Choice for Large Teams
import 'package:flutter_bloc/flutter_bloc.dart';
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
// Usage
BlocProvider(
create: (context) => CounterBloc(),
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: const Text('Increment'),
),
],
);
},
),
);
Bloc enforces a strict separation between events, business logic, and UI. It has a steeper learning curve than Provider or Riverpod, but many larger companies prefer it because it keeps big codebases predictable across teams.
Which one should you learn first? Start with setState, move to Provider once you understand why sharing state across widgets is painful, then learn Riverpod or Bloc depending on what the companies you are targeting use. Check job postings, both are common in production Indian and global teams.
Free Resources:
- Provider Documentation, free
- Riverpod Documentation, free, excellent official docs
- Bloc Library Documentation, free
- Flutter State Management Explained, Reso Coder, free YouTube channel
Phase 6: Networking and APIs (3 to 4 weeks)
The http Package
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<Post>> fetchPosts() async {
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('Failed to load posts');
}
}
Future<void> createPost(String title) async {
await http.post(
Uri.parse('https://api.example.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': title}),
);
}
JSON Serialization
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'title': title, 'body': body};
}
}
For larger apps, use the json_serializable package to auto generate this boilerplate from annotations instead of writing it by hand.
Dio, a More Powerful HTTP Client
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 5),
headers: {'Authorization': 'Bearer $token'},
));
// Interceptors, run on every request or response
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('Request: ${options.path}');
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// refresh token, retry, or log out
}
handler.next(error);
},
));
final response = await dio.get('/posts');
Dio supports interceptors, request cancellation, file uploads with progress, and automatic retries, which is why most production apps prefer it over the plain http package once the app grows beyond a few screens.
Displaying Async Data with FutureBuilder
FutureBuilder<List<Post>>(
future: fetchPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => Text(posts[index].title),
);
}
},
);
Free Resources:
- http package Documentation, free
- Dio Documentation, free
- Networking in Flutter, official cookbook, free
Phase 7: Local Storage and Databases (2 to 3 weeks)
shared_preferences, for Simple Key Value Data
import 'package:shared_preferences/shared_preferences.dart';
Future<void> saveUsername(String name) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', name);
}
Future<String?> getUsername() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('username');
}
Use this for settings, tokens, and small flags. It is not meant for structured or large amounts of data.
sqflite, for Relational Data
import 'package:sqflite/sqflite.dart';
Future<Database> openAppDatabase() async {
return openDatabase(
'app.db',
version: 1,
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, email TEXT)',
);
},
);
}
Future<void> insertUser(Database db, Map<String, dynamic> user) async {
await db.insert('users', user);
}
Future<List<Map<String, dynamic>>> getUsers(Database db) async {
return db.query('users');
}
Hive and Isar, Modern NoSQL Alternatives
import 'package:hive/hive.dart';
// Fast, key value based, works well for caching and offline data
final box = await Hive.openBox('userBox');
box.put('name', 'Rahul');
final name = box.get('name');
Hive and Isar are both faster than sqflite for many use cases and do not require raw SQL, which is why a lot of newer Flutter apps reach for them first.
Free Resources:
- shared_preferences Documentation, free
- sqflite Documentation, free
- Hive Documentation, free
Phase 8: Firebase Integration (3 to 4 weeks)
Firebase is Google's backend as a service platform, and it pairs naturally with Flutter since both come from Google. Most Flutter apps that need a backend without building one from scratch use Firebase.
Setup
dart pub global activate flutterfire_cli
flutterfire configure
This connects your Flutter project to a Firebase project and generates the config files automatically.
Firebase Authentication
import 'package:firebase_auth/firebase_auth.dart';
Future<UserCredential> signUp(String email, String password) async {
return FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
}
Future<UserCredential> signIn(String email, String password) async {
return FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
}
// Listen to auth state changes across the app
StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return const HomeScreen();
}
return const LoginScreen();
},
);
Cloud Firestore, a Real Time NoSQL Database
import 'package:cloud_firestore/cloud_firestore.dart';
final firestore = FirebaseFirestore.instance;
// Write
await firestore.collection('posts').add({
'title': 'My First Post',
'createdAt': FieldValue.serverTimestamp(),
});
// Read once
final snapshot = await firestore.collection('posts').get();
// Real time listener, UI updates automatically when data changes
StreamBuilder<QuerySnapshot>(
stream: firestore.collection('posts').orderBy('createdAt', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final docs = snapshot.data!.docs;
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) => Text(docs[index]['title']),
);
},
);
Cloud Messaging, for Push Notifications
import 'package:firebase_messaging/firebase_messaging.dart';
Future<void> setupNotifications() async {
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final token = await messaging.getToken();
FirebaseMessaging.onMessage.listen((message) {
print('Notification received: ${message.notification?.title}');
});
}
Free Resources:
- FlutterFire Documentation, official, free
- Firebase for Flutter, freeCodeCamp, free full course
Phase 9: Animations (2 to 3 weeks)
Implicit Animations, the Easy Way
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: isExpanded ? 200 : 100,
height: isExpanded ? 200 : 100,
color: isExpanded ? Colors.blue : Colors.red,
curve: Curves.easeInOut,
);
AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: const Text('Fading text'),
);
Implicit animations automatically animate between the old and new values whenever the widget rebuilds with different properties. Reach for these first, they solve most simple animation needs.
Explicit Animations, for Full Control
class SpinningIcon extends StatefulWidget {
const SpinningIcon({super.key});
@override
State<SpinningIcon> createState() => _SpinningIconState();
}
class _SpinningIconState extends State<SpinningIcon>
with SingleTickerProviderStateMixin {
late final AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: controller,
child: const Icon(Icons.refresh, size: 48),
);
}
}
Hero Animations, for Screen Transitions
// On the list screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl),
);
// On the details screen, use the same tag
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, width: 300),
);
When you navigate between these two screens, Flutter automatically animates the image flying from its position on the first screen to its position on the second.
Free Resources:
- Flutter Animations Tutorial, official
- Lottie for Flutter, free, play After Effects animations exported as JSON
Phase 10: Testing in Flutter (2 to 3 weeks)
Unit Tests
import 'package:flutter_test/flutter_test.dart';
int add(int a, int b) => a + b;
void main() {
test('add returns the sum of two numbers', () {
expect(add(2, 3), 5);
});
group('Calculator tests', () {
test('handles negative numbers', () {
expect(add(-1, -1), -2);
});
});
}
Widget Tests
testWidgets('Counter increments when button is tapped', (tester) async {
await tester.pumpWidget(const MaterialApp(home: Counter()));
expect(find.text('Count: 0'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});
Integration Tests
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('full login flow', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
});
}
Run all tests with flutter test. Integration tests run on a real device or emulator and verify the whole app end to end, which is closer to how a QA engineer would test it manually.
Free Resources:
- Testing Flutter Apps, official guide covering all three test types
- mockito Documentation, free, for mocking dependencies in unit tests
Phase 11: Platform Channels and Native Integration (2 weeks)
Sometimes Flutter does not have a plugin for what you need, and you have to write native code directly.
import 'package:flutter/services.dart';
class BatteryService {
static const platform = MethodChannel('com.example.app/battery');
Future<int> getBatteryLevel() async {
try {
final int result = await platform.invokeMethod('getBatteryLevel');
return result;
} on PlatformException catch (e) {
throw Exception('Failed to get battery level: ${e.message}');
}
}
}
On the Android side, in Kotlin, you would implement a matching MethodChannel handler that reads the real battery level and sends it back. The same pattern applies on iOS in Swift.
In practice, most apps never need this. There are over 30,000 packages on pub.dev, Flutter's free package repository, covering nearly every common integration, from camera access to Bluetooth to biometric authentication. Only reach for platform channels when a well maintained package genuinely does not exist.
Free Resources:
- Platform Channels Guide, official
- pub.dev, search before writing custom native code
Phase 12: Publishing, CI/CD, and Performance (2 to 3 weeks)
Building for Release
# Android, generates a signed app bundle for the Play Store
flutter build appbundle --release
# iOS, requires macOS and Xcode
flutter build ipa --release
# Web
flutter build web --release
App Signing
For Android, generate a keystore and reference it in android/key.properties. For iOS, set up a certificate and provisioning profile through your Apple Developer account. Both platforms require this before you can publish.
CI/CD
# .github/workflows/build.yml, a simple GitHub Actions pipeline
name: Build and Test
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- run: flutter pub get
- run: flutter test
- run: flutter build apk --release
Codemagic is a Flutter specific CI/CD service with a generous free tier built specifically for building, testing, and publishing Flutter apps without manual configuration.
Performance
- Use
constconstructors wherever possible, they let Flutter skip rebuilding widgets that have not changed - Avoid rebuilding large widget trees unnecessarily, break big
buildmethods into smaller widgets - Use
ListView.builderinstead ofListViewfor long lists, it only builds visible items - Profile your app with Flutter DevTools, free, built into the SDK, shows frame rendering times, memory usage, and widget rebuild counts
Free Resources:
- Deployment Guide, official, covers both Play Store and App Store
- Codemagic, free tier available for Flutter CI/CD
- Flutter DevTools, free, ships with the SDK
Projects to Build
Beginner
- Calculator app, practice widget composition and state without any external packages
- Todo list app, add, complete, and delete tasks, persist them with
shared_preferences - Weather app, call a free weather API, display formatted results with icons
Intermediate
- E-commerce app, product listing, cart, checkout flow, Firebase for products and orders
- Chat app, Firebase Authentication plus Firestore real time listeners for messages
- News reader, fetch articles from a free news API, save favourites locally with Hive
Advanced
- Social media clone, posts, likes, comments, image upload, real time feed, Riverpod or Bloc for state
- Food delivery app, multi role app with customer and restaurant views, live order tracking, maps integration
- Multi platform app, one codebase targeting mobile, web, and desktop, with responsive layouts and platform specific adjustments
Free YouTube Channels and Resources
| Resource | Best For |
|---|---|
| Flutter, official YouTube channel | Official updates, widget of the week |
| The Net Ninja, Flutter playlist | Beginners, step by step fundamentals |
| Reso Coder | State management, clean architecture |
| Flutter Mapp | Full app build tutorials |
| Robert Brunhage | Advanced patterns, Firebase, backend |
| Code With Andrea | Riverpod, testing, production practices |
Suggested Timeline
| Phase | Topic | Duration |
|---|---|---|
| 1 | Dart Fundamentals | 3 to 4 weeks |
| 2 | Flutter Fundamentals and Widgets | 3 to 4 weeks |
| 3 | Layouts and Responsive UI | 3 to 4 weeks |
| 4 | Navigation and Routing | 2 to 3 weeks |
| 5 | State Management | 4 to 5 weeks |
| 6 | Networking and APIs | 3 to 4 weeks |
| 7 | Local Storage and Databases | 2 to 3 weeks |
| 8 | Firebase Integration | 3 to 4 weeks |
| 9 | Animations | 2 to 3 weeks |
| 10 | Testing | 2 to 3 weeks |
| 11 | Platform Channels and Native Integration | 2 weeks |
| 12 | Publishing, CI/CD, and Performance | 2 to 3 weeks |
Total: 9 to 12 months of consistent part time learning, 2 to 3 hours a day. Full time? Cut it in half.
Flutter Interview Prep
Most Asked Questions
Fundamentals:
- What is the difference between StatelessWidget and StatefulWidget?
- What does
setStateactually do internally? - What is null safety in Dart, and why does it matter?
- What is the difference between
constandfinal? - Explain the widget tree, element tree, and render tree.
Layouts:
- What is the difference between
ExpandedandFlexible? - How does Flutter decide how much space a widget gets?
- What causes a
RenderFlex overflowederror, and how do you fix it?
State Management:
- When would you choose Provider over Bloc, or the reverse?
- What problem does Riverpod solve that Provider does not?
- What is a
ChangeNotifier, and how doesnotifyListenerswork?
Async and Networking:
- What is the difference between a
Futureand aStream? - How do you handle errors in an async function?
- What does
FutureBuilderdo, and what are its main states?
Performance:
- Why should you use
constconstructors when possible? - What is the purpose of keys in Flutter, and when do you need them?
- How do you profile a janky animation using DevTools?
Free Prep:
- Flutter Interview Questions, GeeksforGeeks, free
- Flutter FAQ, official, free, covers common conceptual questions
- Exercism Dart Track, free coding exercises reviewed by mentors
Career Paths
Flutter Developer Build and maintain cross platform mobile apps for startups and companies that want one team shipping to both iOS and Android.
Mobile App Developer Broader mobile role, often expected to also know some native Android or iOS, useful when a Flutter app needs a platform specific feature.
Full Stack Mobile Developer Pair Flutter on the frontend with Firebase, Supabase, or a custom backend you build yourself, common at early stage startups that need one person to own the whole app.
Freelance App Developer Flutter's single codebase advantage makes it especially attractive for freelancers building apps for small businesses that cannot afford separate iOS and Android teams.
Flutter Consultant or Agency Developer Companies like Very Good Ventures build their entire business on Flutter expertise, taking on client apps that need production quality Flutter work.
One Last Thing
Flutter has a reputation for being easy to pick up, and it genuinely is easier than learning two native platforms from scratch. But do not mistake easy to start for easy to master.
The developers who get hired are the ones who understand why their widget rebuilds too often, who can debug a layout overflow without panicking, and who know when to reach for Riverpod instead of just piling more setState calls into a growing widget. That depth comes from building real things, not from finishing tutorials.
Build the calculator. Build the todo app. Then build something nobody asked you to build, something with a real backend, real users, and real bugs you have to fix yourself. That is where the actual learning happens.
Start at flutter.dev right now. It is free, it runs on any operating system, and you can have your first app running on an emulator within an hour.
Join our Telegram group to connect with other Flutter developers and get help!