Back to all articles

Flutter Mobile Development: Complete Guide 2025

Flutter, Google's UI toolkit, powers over 500,000 apps including Google Ads, Alibaba, BMW, and eBay. With 46% of developers choosing it for cross-platform development, Flutter offers native performance, beautiful UI, and incredibly fast development. Build stunning apps for iOS, Android, web, and desktop from a single codebase.

Why Flutter?

Key Advantages

  • Fast development: Hot reload in milliseconds
  • Native performance: Compiled to native ARM code
  • Beautiful UI: Rich, customizable widgets
  • Single codebase: iOS, Android, web, desktop
  • Strong typing: Dart language catches errors early
  • Growing ecosystem: 30,000+ packages on pub.dev
  • Google backing: Long-term support and investment

Flutter vs React Native

Flutter:
✓ Slightly better performance (native compiled)
✓ Consistent UI across platforms
✓ Built-in Material Design & Cupertino widgets
✓ Faster hot reload
✓ Better documentation
✗ Dart learning curve
✗ Smaller community than React
✗ Larger app size

React Native:
✓ JavaScript/TypeScript (more familiar)
✓ Larger ecosystem (npm)
✓ Huge community
✓ React knowledge transferable
✗ Bridge architecture (performance)
✗ More platform-specific code needed
✗ UI consistency requires work

Both are excellent choices. Choose Flutter for:
- Completely custom UI
- Maximum performance
- Starting fresh project
- Smaller team

Choose React Native for:
- Existing React developers
- Large JS ecosystem needs
- Web app already in React

Setup and Installation

Install Flutter

macOS Installation

# Download Flutter SDK
# Visit https://flutter.dev/docs/get-started/install/macos
# Or use git:
cd ~/development
git clone https://github.com/flutter/flutter.git -b stable

# Add to PATH
export PATH="$PATH:`pwd`/flutter/bin"
# Add to ~/.zshrc for permanent

# Run doctor to check installation
flutter doctor

# Install Xcode (from App Store)
# Install Xcode command line tools
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

# Accept licenses
sudo xcodebuild -license

# Install CocoaPods
sudo gem install cocoapods

# Install Android Studio
# Download from https://developer.android.com/studio
# Install Android SDK through Android Studio
# Configure ANDROID_HOME
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools

# Run flutter doctor again
flutter doctor

# Should show checkmarks for:
# ✓ Flutter
# ✓ Android toolchain
# ✓ Xcode
# ✓ Chrome (for web)
# ✓ Android Studio
# ✓ VS Code (if installed)

Create First Project

# Create new Flutter app
flutter create my_app
cd my_app

# Project structure
my_app/
├── android/          # Android-specific code
├── ios/             # iOS-specific code
├── lib/             # Your Dart code
│   └── main.dart    # Entry point
├── test/            # Tests
├── pubspec.yaml     # Dependencies
└── README.md

# Run on iOS simulator
flutter run -d "iPhone 15 Pro"

# Run on Android emulator
flutter run -d emulator-5554

# Run on all connected devices
flutter run -d all

# Hot reload: Press 'r' in terminal
# Hot restart: Press 'R'
# Quit: Press 'q'

# Build for release
flutter build ios
flutter build apk
flutter build appbundle

Dart Basics

Essential Dart Syntax

// Variables
var name = ''John'';      // Type inference
String name = ''John'';   // Explicit type
final name = ''John'';    // Runtime constant
const pi = 3.14159;      // Compile-time constant

// Null safety
String? nullable;         // Can be null
String nonNull = ''text''; // Cannot be null
nullable?.length;         // Null-aware operator
nullable ?? ''default'';   // Null coalescing

// Functions
String greet(String name) {
  return ''Hello, $name'';
}

// Arrow functions
String greet(String name) => ''Hello, $name'';

// Named parameters
void printUser({required String name, int age = 0}) {
  print(''$name is $age years old'');
}
printUser(name: ''John'', age: 30);

// Optional parameters
void printUser(String name, [int? age]) {
  print(name);
}

// Collections
List names = [''John'', ''Jane'', ''Bob''];
Map ages = {''John'': 30, ''Jane'': 25};
Set numbers = {1, 2, 3};

// Spread operator
var list1 = [1, 2, 3];
var list2 = [0, ...list1, 4]; // [0, 1, 2, 3, 4]

// Collection if
var nav = [
  ''Home'',
  if (isLoggedIn) ''Profile'',
  ''Settings'',
];

// Classes
class User {
  final String id;
  final String name;
  
  User({required this.id, required this.name});
  
  // Named constructor
  User.guest() : id = ''guest'', name = ''Guest User'';
  
  // Method
  void greet() {
    print(''Hello, $name'');
  }
}

// Async/await
Future fetchData() async {
  final response = await http.get(Uri.parse(''https://api.example.com''));
  return response.body;
}

// Streams
Stream countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

Widgets

Stateless vs Stateful Widgets

// Stateless Widget (doesn''t change)
class MyButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  
  const MyButton({
    Key? key,
    required this.text,
    required this.onPressed,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(text),
    );
  }
}

// Stateful Widget (can change)
class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);
  
  @override
  State createState() => _CounterState();
}

class _CounterState extends State {
  int _count = 0;
  
  void _increment() {
    setState(() {
      _count++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(''Count: $_count''),
        ElevatedButton(
          onPressed: _increment,
          child: Text(''Increment''),
        ),
      ],
    );
  }
}

Common Widgets

// Layout Widgets
Container(
  width: 200,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(horizontal: 20),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
    boxShadow: [
      BoxShadow(
        color: Colors.black26,
        blurRadius: 4,
        offset: Offset(0, 2),
      ),
    ],
  ),
  child: Text(''Hello''),
)

// Row (horizontal)
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Icon(Icons.star),
    Text(''Title''),
    Icon(Icons.more_vert),
  ],
)

// Column (vertical)
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text(''Title''),
    Text(''Subtitle''),
    ElevatedButton(
      onPressed: () {},
      child: Text(''Press Me''),
    ),
  ],
)

// Stack (overlapping)
Stack(
  children: [
    Image.network(''https://example.com/image.jpg''),
    Positioned(
      bottom: 16,
      right: 16,
      child: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.edit),
      ),
    ),
  ],
)

// ListView (scrollable list)
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: Icon(Icons.person),
      title: Text(items[index].title),
      subtitle: Text(items[index].subtitle),
      trailing: Icon(Icons.arrow_forward_ios),
      onTap: () {
        // Handle tap
      },
    );
  },
)

// GridView
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
  ),
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Card(
      child: Center(
        child: Text(items[index]),
      ),
    );
  },
)

// Text
Text(
  ''Hello World'',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

// Image
Image.network(
  ''https://example.com/image.jpg'',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
)

// TextField
TextField(
  decoration: InputDecoration(
    labelText: ''Email'',
    hintText: ''Enter your email'',
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(),
  ),
  keyboardType: TextInputType.emailAddress,
  onChanged: (value) {
    print(value);
  },
)

Navigation

Basic Navigation

// Navigate to new screen
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);

// Navigate back
Navigator.pop(context);

// Navigate and return data
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);
print(result);

// Pop with data
Navigator.pop(context, ''Data from second screen'');

// Replace current screen
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomeScreen()),
);

// Named routes
// Define in MaterialApp
MaterialApp(
  initialRoute: ''/'',
  routes: {
    ''/'': (context) => HomeScreen(),
    ''/profile'': (context) => ProfileScreen(),
    ''/settings'': (context) => SettingsScreen(),
  },
)

// Navigate using named route
Navigator.pushNamed(context, ''/profile'');

// Pass arguments
Navigator.pushNamed(
  context,
  ''/profile'',
  arguments: {''userId'': ''123''},
);

// Receive arguments
final args = ModalRoute.of(context)!.settings.arguments as Map;
final userId = args[''userId''];

GoRouter (Recommended)

# Add dependency
dependencies:
  go_router: ^13.0.0

// router.dart
import ''package:go_router/go_router.dart'';

final router = GoRouter(
  initialLocation: ''/'',
  routes: [
    GoRoute(
      path: ''/'',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      path: ''/profile/:userId'',
      builder: (context, state) {
        final userId = state.pathParameters[''userId'']!;
        return ProfileScreen(userId: userId);
      },
    ),
    GoRoute(
      path: ''/settings'',
      builder: (context, state) => SettingsScreen(),
    ),
  ],
);

// main.dart
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

// Navigate
context.go(''/profile/123'');
context.push(''/settings'');
context.pop();

State Management

Provider (Recommended for Beginners)

# Add dependency
dependencies:
  provider: ^6.1.0

// user_provider.dart
import ''package:flutter/foundation.dart'';

class UserProvider extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;
  
  User? get user => _user;
  bool get isLoading => _isLoading;
  bool get isAuthenticated => _user != null;
  
  Future login(String email, String password) async {
    _isLoading = true;
    notifyListeners();
    
    try {
      // API call
      final user = await AuthService.login(email, password);
      _user = user;
    } catch (e) {
      rethrow;
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
  
  void logout() {
    _user = null;
    notifyListeners();
  }
}

// main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserProvider()),
        ChangeNotifierProvider(create: (_) => CartProvider()),
      ],
      child: MyApp(),
    ),
  );
}

// Usage in widget
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userProvider = Provider.of(context);
    
    // Or using Consumer for rebuilds
    return Consumer(
      builder: (context, userProvider, child) {
        if (userProvider.isLoading) {
          return CircularProgressIndicator();
        }
        
        final user = userProvider.user;
        if (user == null) {
          return Text(''Please log in'');
        }
        
        return Column(
          children: [
            Text(''Welcome, ${user.name}''),
            ElevatedButton(
              onPressed: userProvider.logout,
              child: Text(''Logout''),
            ),
          ],
        );
      },
    );
  }
}

Riverpod (Modern Alternative)

# Add dependency
dependencies:
  flutter_riverpod: ^2.4.0

// providers.dart
import ''package:flutter_riverpod/flutter_riverpod.dart'';

// Simple provider
final counterProvider = StateProvider((ref) => 0);

// Future provider
final userProvider = FutureProvider((ref) async {
  final response = await http.get(Uri.parse(''https://api.example.com/user''));
  return User.fromJson(jsonDecode(response.body));
});

// StateNotifier provider
class CounterNotifier extends StateNotifier {
  CounterNotifier() : super(0);
  
  void increment() => state++;
  void decrement() => state--;
}

final counterNotifierProvider = StateNotifierProvider(
  (ref) => CounterNotifier(),
);

// main.dart
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

// Usage
class CounterScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterNotifierProvider);
    
    return Column(
      children: [
        Text(''Count: $count''),
        ElevatedButton(
          onPressed: () => ref.read(counterNotifierProvider.notifier).increment(),
          child: Text(''Increment''),
        ),
      ],
    );
  }
}

HTTP Requests

Using http Package

# Add dependency
dependencies:
  http: ^1.1.0

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

class ApiService {
  static const String baseUrl = ''https://api.example.com'';
  
  static Future> fetchUsers() async {
    final response = await http.get(
      Uri.parse(''$baseUrl/users''),
      headers: {
        ''Content-Type'': ''application/json'',
      },
    );
    
    if (response.statusCode == 200) {
      final List data = jsonDecode(response.body);
      return data.map((json) => User.fromJson(json)).toList();
    } else {
      throw Exception(''Failed to load users'');
    }
  }
  
  static Future createUser(User user) async {
    final response = await http.post(
      Uri.parse(''$baseUrl/users''),
      headers: {
        ''Content-Type'': ''application/json'',
        ''Authorization'': ''Bearer $token'',
      },
      body: jsonEncode(user.toJson()),
    );
    
    if (response.statusCode == 201) {
      return User.fromJson(jsonDecode(response.body));
    } else {
      throw Exception(''Failed to create user'');
    }
  }
}

// Usage in widget
class UsersScreen extends StatefulWidget {
  @override
  State createState() => _UsersScreenState();
}

class _UsersScreenState extends State {
  List users = [];
  bool isLoading = true;
  String? error;
  
  @override
  void initState() {
    super.initState();
    loadUsers();
  }
  
  Future loadUsers() async {
    try {
      final data = await ApiService.fetchUsers();
      setState(() {
        users = data;
        isLoading = false;
      });
    } catch (e) {
      setState(() {
        error = e.toString();
        isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (isLoading) {
      return Center(child: CircularProgressIndicator());
    }
    
    if (error != null) {
      return Center(child: Text(''Error: $error''));
    }
    
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        final user = users[index];
        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
        );
      },
    );
  }
}

Using Dio (Advanced)

# Add dependency
dependencies:
  dio: ^5.4.0

// api_client.dart
import ''package:dio/dio.dart'';

class ApiClient {
  static final ApiClient _instance = ApiClient._internal();
  factory ApiClient() => _instance;
  
  late Dio dio;
  
  ApiClient._internal() {
    dio = Dio(
      BaseOptions(
        baseUrl: ''https://api.example.com'',
        connectTimeout: Duration(seconds: 10),
        receiveTimeout: Duration(seconds: 10),
        headers: {
          ''Content-Type'': ''application/json'',
        },
      ),
    );
    
    // Request interceptor
    dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          final token = getAuthToken();
          if (token != null) {
            options.headers[''Authorization''] = ''Bearer $token'';
          }
          return handler.next(options);
        },
        onError: (error, handler) {
          if (error.response?.statusCode == 401) {
            // Handle unauthorized
            logout();
          }
          return handler.next(error);
        },
      ),
    );
  }
  
  Future> fetchUsers() async {
    try {
      final response = await dio.get(''/users'');
      final List data = response.data;
      return data.map((json) => User.fromJson(json)).toList();
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }
  
  Exception _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        return Exception(''Connection timeout'');
      case DioExceptionType.receiveTimeout:
        return Exception(''Receive timeout'');
      case DioExceptionType.badResponse:
        return Exception(''Server error: ${error.response?.statusCode}'');
      default:
        return Exception(''Network error'');
    }
  }
}

Local Storage

Shared Preferences

# Add dependency
dependencies:
  shared_preferences: ^2.2.0

// storage_service.dart
import ''package:shared_preferences/shared_preferences.dart'';

class StorageService {
  static Future saveString(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }
  
  static Future getString(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
  
  static Future saveBool(String key, bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(key, value);
  }
  
  static Future getBool(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(key);
  }
  
  static Future remove(String key) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(key);
  }
  
  static Future clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
  }
}

// Usage
await StorageService.saveString(''token'', ''abc123'');
final token = await StorageService.getString(''token'');

Animations

Implicit Animations

// AnimatedContainer
class AnimatedBox extends StatefulWidget {
  @override
  State createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State {
  bool _expanded = false;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _expanded ? 200 : 100,
        height: _expanded ? 200 : 100,
        color: _expanded ? Colors.blue : Colors.red,
        child: Center(
          child: Text(''Tap me''),
        ),
      ),
    );
  }
}

// AnimatedOpacity
AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: Duration(milliseconds: 500),
  child: Text(''Fade in/out''),
)

// AnimatedCrossFade
AnimatedCrossFade(
  firstChild: Icon(Icons.play_arrow),
  secondChild: Icon(Icons.pause),
  crossFadeState: _isPlaying 
    ? CrossFadeState.showSecond 
    : CrossFadeState.showFirst,
  duration: Duration(milliseconds: 300),
)

Explicit Animations

class FadeAnimation extends StatefulWidget {
  final Widget child;
  
  const FadeAnimation({required this.child});
  
  @override
  State createState() => _FadeAnimationState();
}

class _FadeAnimationState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    _animation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeIn),
    );
    
    _controller.forward();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation,
      child: widget.child,
    );
  }
}

Testing

Unit Tests

# Add dev dependencies
dev_dependencies:
  flutter_test:
    sdk: flutter

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

void main() {
  group(''User'', () {
    test(''fromJson creates User correctly'', () {
      final json = {
        ''id'': ''1'',
        ''name'': ''John Doe'',
        ''email'': ''[email protected]'',
      };
      
      final user = User.fromJson(json);
      
      expect(user.id, ''1'');
      expect(user.name, ''John Doe'');
      expect(user.email, ''[email protected]'');
    });
    
    test(''toJson creates correct JSON'', () {
      final user = User(
        id: ''1'',
        name: ''John Doe'',
        email: ''[email protected]'',
      );
      
      final json = user.toJson();
      
      expect(json[''id''], ''1'');
      expect(json[''name''], ''John Doe'');
      expect(json[''email''], ''[email protected]'');
    });
  });
}

// Run tests
flutter test

Widget Tests

// test/counter_test.dart
import ''package:flutter_test/flutter_test.dart'';
import ''package:flutter/material.dart'';

void main() {
  testWidgets(''Counter increments'', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(home: Counter()));
    
    expect(find.text(''0''), findsOneWidget);
    expect(find.text(''1''), findsNothing);
    
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    expect(find.text(''0''), findsNothing);
    expect(find.text(''1''), findsOneWidget);
  });
}

Build and Deploy

iOS Deployment

# Build release
flutter build ios --release

# Or build IPA
flutter build ipa

# Open Xcode for archiving
open ios/Runner.xcworkspace

# In Xcode:
# 1. Select "Any iOS Device"
# 2. Product > Archive
# 3. Distribute App
# 4. Upload to App Store Connect

Android Deployment

# Generate keystore
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload

# Create android/key.properties
storePassword=
keyPassword=
keyAlias=upload
storeFile=

# Build APK
flutter build apk --release

# Build App Bundle (recommended)
flutter build appbundle --release

# Output:
# APK: build/app/outputs/flutter-apk/app-release.apk
# AAB: build/app/outputs/bundle/release/app-release.aab

Conclusion

Flutter empowers developers to build beautiful, fast, and native-quality apps from a single codebase. With hot reload, rich widgets, strong typing, and excellent documentation, Flutter makes mobile development productive and enjoyable. Whether you''re building your first app or your hundredth, Flutter provides the tools and performance you need to succeed.

Every Flutter app needs professional support infrastructure. Our generator creates beautiful, responsive support pages optimized for mobile that integrate perfectly with your Flutter app and meet all app store requirements.

Need a Support URL for Your App?

Generate a compliant, professional support page in under a minute. Our easy-to-use generator creates everything you need for App Store and Google Play submissions.