Back to Blog
Development Tips18 min read

Flutter Tutorial 2026: Build Your First Android & iOS App

Complete Flutter tutorial for beginners. Learn Dart, widgets, state management with Riverpod, navigation, API integration, and build a production-ready app. Includes Android/iOS setup.

Hevcode Team
January 5, 2026

Flutter powers apps used by Google, BMW, Alibaba, and eBay. With a single codebase, you can build beautiful, natively compiled apps for mobile, web, and desktop. This tutorial takes you from zero to a production-ready app.

Why Flutter in 2026?

Framework Performance Dev Speed Code Sharing Learning Curve
Flutter Excellent Fast 95%+ Moderate
React Native Good Fast 90%+ Easy (if know JS)
Native Best Slow 0% Steep

Flutter Advantages

  • True cross-platform: iOS, Android, Web, Windows, macOS, Linux
  • Hot reload: See changes in milliseconds
  • Beautiful UIs: Material Design 3 and Cupertino widgets
  • Dart language: Easy to learn, strongly typed
  • Growing fast: 42% market share among cross-platform frameworks
  • Google backing: Long-term support and updates

Prerequisites

  • Basic programming knowledge (any language)
  • Computer with Windows, macOS, or Linux
  • 2-3 hours for this tutorial

Setting Up Flutter

macOS Setup

# Install with Homebrew (recommended)
brew install --cask flutter

# Add to PATH (add to ~/.zshrc)
export PATH="$PATH:$HOME/flutter/bin"

# Verify installation
flutter doctor

Windows Setup

# 1. Download Flutter SDK from flutter.dev
# 2. Extract to C:\src\flutter
# 3. Add to PATH:
#    - Open System Properties > Environment Variables
#    - Add C:\src\flutter\bin to Path

# 4. Verify installation
flutter doctor

Linux Setup

# Ubuntu/Debian
sudo snap install flutter --classic

# Or manual installation
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev
git clone https://github.com/flutter/flutter.git ~/flutter
export PATH="$PATH:$HOME/flutter/bin"

flutter doctor

Android Setup (All Platforms)

# 1. Install Android Studio from developer.android.com
# 2. Open Android Studio > SDK Manager
# 3. Install:
#    - Android SDK Platform 34
#    - Android SDK Build-Tools 34
#    - Android SDK Command-line Tools
#    - Android Emulator

# 4. Accept licenses
flutter doctor --android-licenses

# 5. Create emulator
flutter emulators --create --name Pixel_7

# 6. Run emulator
flutter emulators --launch Pixel_7

iOS Setup (macOS Only)

# 1. Install Xcode from App Store

# 2. Install command line tools
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

# 3. Install CocoaPods
sudo gem install cocoapods

# 4. Open iOS Simulator
open -a Simulator

# 5. Verify
flutter doctor

Create Your First Project

flutter create my_app
cd my_app
flutter run

Project Structure

my_app/
├── lib/
│   ├── main.dart              # Entry point
│   ├── models/                # Data models
│   ├── screens/               # Screen widgets
│   ├── widgets/               # Reusable widgets
│   ├── services/              # API, database
│   ├── providers/             # State management
│   └── utils/                 # Helpers
├── assets/                    # Images, fonts
├── test/                      # Tests
├── android/                   # Android native code
├── ios/                       # iOS native code
├── pubspec.yaml              # Dependencies
└── analysis_options.yaml     # Linter rules

Dart Language Essentials

Variables & Types

// Type inference
var name = 'John';      // String
var age = 25;           // int
var price = 29.99;      // double
var isActive = true;    // bool

// Explicit types
String city = 'New York';
int score = 100;
double rating = 4.5;
bool isStudent = false;

// Null safety (Dart 3+)
String? nullableName;           // Can be null
String nonNullable = 'Value';   // Cannot be null
int count = nullableName?.length ?? 0;  // Null-aware operators

// Late initialization
late String description;        // Initialized later

// Final vs Const
final createdAt = DateTime.now();  // Runtime constant
const pi = 3.14159;                // Compile-time constant

Functions

// Basic function
int add(int a, int b) {
  return a + b;
}

// Arrow function
int multiply(int a, int b) => a * b;

// Named parameters (most common in Flutter)
void createUser({
  required String name,
  required String email,
  int age = 18,
  bool isAdmin = false,
}) {
  print('Creating user: $name');
}

// Usage
createUser(name: 'John', email: 'john@example.com');
createUser(name: 'Jane', email: 'jane@example.com', isAdmin: true);

// Optional positional parameters
String greet(String name, [String? title]) {
  return title != null ? '$title $name' : name;
}

Classes

class User {
  final String id;
  final String name;
  final String email;
  int age;

  // Primary constructor
  User({
    required this.id,
    required this.name,
    required this.email,
    this.age = 18,
  });

  // Named constructor
  User.guest()
      : id = 'guest',
        name = 'Guest User',
        email = 'guest@example.com',
        age = 0;

  // Factory constructor (for JSON parsing)
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      age: json['age'] as int? ?? 18,
    );
  }

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

  // Getter
  bool get isAdult => age >= 18;

  // Override toString
  @override
  String toString() => 'User(name: $name, email: $email)';
}

Collections

// List
List<String> fruits = ['apple', 'banana', 'orange'];
fruits.add('grape');
fruits.remove('banana');
final firstFruit = fruits.first;
final hasBanana = fruits.contains('banana');

// Map
Map<String, int> scores = {
  'math': 95,
  'science': 88,
  'english': 92,
};
scores['history'] = 85;
final mathScore = scores['math'] ?? 0;

// Set (unique values)
Set<int> numbers = {1, 2, 3, 4, 5};
numbers.add(3);  // No duplicate added

// Collection operations
final doubled = [1, 2, 3].map((n) => n * 2).toList();
final evens = [1, 2, 3, 4, 5].where((n) => n.isEven).toList();
final sum = [1, 2, 3, 4, 5].fold(0, (a, b) => a + b);

// Spread operator
final moreFruits = [...fruits, 'mango', 'kiwi'];

// Collection if/for
final items = [
  'item1',
  if (true) 'conditionalItem',
  for (var i = 1; i <= 3; i++) 'item$i',
];

Async/Await

// Async function
Future<String> fetchUserName(String id) async {
  // Simulate network delay
  await Future.delayed(const Duration(seconds: 1));
  return 'John Doe';
}

// Usage
Future<void> loadUser() async {
  try {
    final name = await fetchUserName('123');
    print('User: $name');
  } catch (e) {
    print('Error: $e');
  }
}

// Parallel execution
Future<void> loadMultiple() async {
  final results = await Future.wait([
    fetchUserName('1'),
    fetchUserName('2'),
    fetchUserName('3'),
  ]);
  print('All users: $results');
}

// Stream
Stream<int> countDown(int from) async* {
  for (var i = from; i >= 0; i--) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

Flutter Widgets

StatelessWidget vs StatefulWidget

// StatelessWidget - UI that doesn't change
class Greeting extends StatelessWidget {
  final String name;

  const Greeting({super.key, required this.name});

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

// StatefulWidget - UI that changes over time
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(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '$_count',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        const SizedBox(height: 16),
        FilledButton.icon(
          onPressed: _increment,
          icon: const Icon(Icons.add),
          label: const Text('Increment'),
        ),
      ],
    );
  }
}

Layout Widgets

// Container with decoration
Container(
  width: 200,
  height: 100,
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.symmetric(horizontal: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 10,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  child: const Text('Hello'),
)

// Column - vertical layout
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Item 1'),
    SizedBox(height: 8),
    Text('Item 2'),
    SizedBox(height: 8),
    Text('Item 3'),
  ],
)

// Row - horizontal layout
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Icon(Icons.star, color: Colors.amber),
    Text('4.5'),
    Text('(128 reviews)'),
  ],
)

// Stack - overlay widgets
Stack(
  children: [
    Image.network('https://example.com/image.jpg'),
    Positioned(
      bottom: 16,
      left: 16,
      right: 16,
      child: Text(
        'Overlay Title',
        style: TextStyle(color: Colors.white, fontSize: 24),
      ),
    ),
  ],
)

// Wrap - flow layout
Wrap(
  spacing: 8,
  runSpacing: 8,
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Mobile')),
  ],
)

Scrolling Widgets

// ListView.builder - efficient for long lists
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text(items[index].title),
      subtitle: Text(items[index].subtitle),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => print('Tapped item $index'),
    );
  },
)

// ListView.separated - with dividers
ListView.separated(
  itemCount: items.length,
  separatorBuilder: (context, index) => const Divider(),
  itemBuilder: (context, index) {
    return ListTile(title: Text(items[index]));
  },
)

// GridView
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
    childAspectRatio: 1,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) {
    return ProductCard(product: products[index]);
  },
)

// CustomScrollView with Slivers
CustomScrollView(
  slivers: [
    SliverAppBar(
      expandedHeight: 200,
      floating: false,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('My App'),
        background: Image.network(
          'https://example.com/header.jpg',
          fit: BoxFit.cover,
        ),
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('Item $index')),
        childCount: 50,
      ),
    ),
  ],
)

Building a Complete Task App

Step 1: Data Model

// lib/models/task.dart
import 'package:flutter/foundation.dart';

@immutable
class Task {
  final String id;
  final String title;
  final String? description;
  final bool isCompleted;
  final DateTime createdAt;
  final DateTime? dueDate;
  final Priority priority;

  const Task({
    required this.id,
    required this.title,
    this.description,
    this.isCompleted = false,
    required this.createdAt,
    this.dueDate,
    this.priority = Priority.medium,
  });

  Task copyWith({
    String? title,
    String? description,
    bool? isCompleted,
    DateTime? dueDate,
    Priority? priority,
  }) {
    return Task(
      id: id,
      title: title ?? this.title,
      description: description ?? this.description,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt,
      dueDate: dueDate ?? this.dueDate,
      priority: priority ?? this.priority,
    );
  }

  Map<String, dynamic> toJson() => {
        'id': id,
        'title': title,
        'description': description,
        'isCompleted': isCompleted,
        'createdAt': createdAt.toIso8601String(),
        'dueDate': dueDate?.toIso8601String(),
        'priority': priority.name,
      };

  factory Task.fromJson(Map<String, dynamic> json) => Task(
        id: json['id'] as String,
        title: json['title'] as String,
        description: json['description'] as String?,
        isCompleted: json['isCompleted'] as bool? ?? false,
        createdAt: DateTime.parse(json['createdAt'] as String),
        dueDate: json['dueDate'] != null
            ? DateTime.parse(json['dueDate'] as String)
            : null,
        priority: Priority.values.byName(json['priority'] as String? ?? 'medium'),
      );
}

enum Priority { low, medium, high }

Step 2: State Management with Riverpod

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9
  shared_preferences: ^2.2.2
// lib/providers/task_provider.dart
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/task.dart';

// Task list state notifier
class TaskNotifier extends StateNotifier<List<Task>> {
  TaskNotifier() : super([]) {
    _loadTasks();
  }

  static const _storageKey = 'tasks';

  Future<void> _loadTasks() async {
    final prefs = await SharedPreferences.getInstance();
    final tasksJson = prefs.getString(_storageKey);
    if (tasksJson != null) {
      final tasksList = jsonDecode(tasksJson) as List;
      state = tasksList.map((json) => Task.fromJson(json)).toList();
    }
  }

  Future<void> _saveTasks() async {
    final prefs = await SharedPreferences.getInstance();
    final tasksJson = jsonEncode(state.map((t) => t.toJson()).toList());
    await prefs.setString(_storageKey, tasksJson);
  }

  void addTask(String title, {String? description, Priority? priority}) {
    final task = Task(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title,
      description: description,
      createdAt: DateTime.now(),
      priority: priority ?? Priority.medium,
    );
    state = [task, ...state];
    _saveTasks();
  }

  void toggleTask(String id) {
    state = state.map((task) {
      if (task.id == id) {
        return task.copyWith(isCompleted: !task.isCompleted);
      }
      return task;
    }).toList();
    _saveTasks();
  }

  void updateTask(String id, {String? title, String? description, Priority? priority}) {
    state = state.map((task) {
      if (task.id == id) {
        return task.copyWith(
          title: title,
          description: description,
          priority: priority,
        );
      }
      return task;
    }).toList();
    _saveTasks();
  }

  void deleteTask(String id) {
    state = state.where((task) => task.id != id).toList();
    _saveTasks();
  }

  void clearCompleted() {
    state = state.where((task) => !task.isCompleted).toList();
    _saveTasks();
  }
}

// Providers
final taskProvider = StateNotifierProvider<TaskNotifier, List<Task>>((ref) {
  return TaskNotifier();
});

// Filtered tasks
enum TaskFilter { all, active, completed }

final taskFilterProvider = StateProvider<TaskFilter>((ref) => TaskFilter.all);

final filteredTasksProvider = Provider<List<Task>>((ref) {
  final tasks = ref.watch(taskProvider);
  final filter = ref.watch(taskFilterProvider);

  switch (filter) {
    case TaskFilter.active:
      return tasks.where((t) => !t.isCompleted).toList();
    case TaskFilter.completed:
      return tasks.where((t) => t.isCompleted).toList();
    case TaskFilter.all:
      return tasks;
  }
});

// Stats
final taskStatsProvider = Provider<({int total, int completed, int active})>((ref) {
  final tasks = ref.watch(taskProvider);
  final completed = tasks.where((t) => t.isCompleted).length;
  return (total: tasks.length, completed: completed, active: tasks.length - completed);
});

Step 3: Main App with Theme

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Task Manager',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        inputDecorationTheme: InputDecorationTheme(
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          filled: true,
        ),
        cardTheme: CardTheme(
          elevation: 0,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}

Step 4: Home Screen

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/task.dart';
import '../providers/task_provider.dart';
import '../widgets/task_tile.dart';
import '../widgets/add_task_sheet.dart';

class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final tasks = ref.watch(filteredTasksProvider);
    final stats = ref.watch(taskStatsProvider);
    final filter = ref.watch(taskFilterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Tasks'),
        actions: [
          PopupMenuButton<TaskFilter>(
            icon: const Icon(Icons.filter_list),
            onSelected: (value) {
              ref.read(taskFilterProvider.notifier).state = value;
            },
            itemBuilder: (context) => [
              PopupMenuItem(
                value: TaskFilter.all,
                child: Row(
                  children: [
                    if (filter == TaskFilter.all) const Icon(Icons.check, size: 18),
                    const SizedBox(width: 8),
                    const Text('All'),
                  ],
                ),
              ),
              PopupMenuItem(
                value: TaskFilter.active,
                child: Row(
                  children: [
                    if (filter == TaskFilter.active) const Icon(Icons.check, size: 18),
                    const SizedBox(width: 8),
                    const Text('Active'),
                  ],
                ),
              ),
              PopupMenuItem(
                value: TaskFilter.completed,
                child: Row(
                  children: [
                    if (filter == TaskFilter.completed) const Icon(Icons.check, size: 18),
                    const SizedBox(width: 8),
                    const Text('Completed'),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
      body: Column(
        children: [
          // Stats bar
          Container(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                _StatCard(
                  label: 'Total',
                  value: stats.total,
                  color: Colors.blue,
                ),
                _StatCard(
                  label: 'Active',
                  value: stats.active,
                  color: Colors.orange,
                ),
                _StatCard(
                  label: 'Done',
                  value: stats.completed,
                  color: Colors.green,
                ),
              ],
            ),
          ),

          // Task list
          Expanded(
            child: tasks.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(
                          Icons.task_alt,
                          size: 64,
                          color: Theme.of(context).colorScheme.outline,
                        ),
                        const SizedBox(height: 16),
                        Text(
                          'No tasks yet',
                          style: Theme.of(context).textTheme.titleMedium?.copyWith(
                                color: Theme.of(context).colorScheme.outline,
                              ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'Tap + to add your first task',
                          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                                color: Theme.of(context).colorScheme.outline,
                              ),
                        ),
                      ],
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    itemCount: tasks.length,
                    itemBuilder: (context, index) {
                      final task = tasks[index];
                      return TaskTile(
                        task: task,
                        onToggle: () {
                          ref.read(taskProvider.notifier).toggleTask(task.id);
                        },
                        onDelete: () {
                          ref.read(taskProvider.notifier).deleteTask(task.id);
                        },
                      );
                    },
                  ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            builder: (context) => const AddTaskSheet(),
          );
        },
        icon: const Icon(Icons.add),
        label: const Text('Add Task'),
      ),
    );
  }
}

class _StatCard extends StatelessWidget {
  final String label;
  final int value;
  final Color color;

  const _StatCard({
    required this.label,
    required this.value,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        child: Column(
          children: [
            Text(
              '$value',
              style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                    color: color,
                    fontWeight: FontWeight.bold,
                  ),
            ),
            Text(
              label,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
      ),
    );
  }
}

Step 5: Task Tile Widget

// lib/widgets/task_tile.dart
import 'package:flutter/material.dart';
import '../models/task.dart';

class TaskTile extends StatelessWidget {
  final Task task;
  final VoidCallback onToggle;
  final VoidCallback onDelete;

  const TaskTile({
    super.key,
    required this.task,
    required this.onToggle,
    required this.onDelete,
  });

  Color _priorityColor(Priority priority) {
    switch (priority) {
      case Priority.high:
        return Colors.red;
      case Priority.medium:
        return Colors.orange;
      case Priority.low:
        return Colors.green;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: Key(task.id),
      direction: DismissDirection.endToStart,
      onDismissed: (_) => onDelete(),
      background: Container(
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 20),
        margin: const EdgeInsets.only(bottom: 8),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(12),
        ),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      child: Card(
        margin: const EdgeInsets.only(bottom: 8),
        child: ListTile(
          leading: Checkbox(
            value: task.isCompleted,
            onChanged: (_) => onToggle(),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(4),
            ),
          ),
          title: Text(
            task.title,
            style: TextStyle(
              decoration: task.isCompleted ? TextDecoration.lineThrough : null,
              color: task.isCompleted
                  ? Theme.of(context).colorScheme.outline
                  : null,
            ),
          ),
          subtitle: task.description != null
              ? Text(
                  task.description!,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: TextStyle(
                    color: Theme.of(context).colorScheme.outline,
                  ),
                )
              : null,
          trailing: Container(
            width: 12,
            height: 12,
            decoration: BoxDecoration(
              color: _priorityColor(task.priority),
              shape: BoxShape.circle,
            ),
          ),
        ),
      ),
    );
  }
}

Step 6: Add Task Bottom Sheet

// lib/widgets/add_task_sheet.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/task.dart';
import '../providers/task_provider.dart';

class AddTaskSheet extends ConsumerStatefulWidget {
  const AddTaskSheet({super.key});

  @override
  ConsumerState<AddTaskSheet> createState() => _AddTaskSheetState();
}

class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();
  Priority _priority = Priority.medium;

  @override
  void dispose() {
    _titleController.dispose();
    _descriptionController.dispose();
    super.dispose();
  }

  void _submit() {
    if (_titleController.text.trim().isEmpty) return;

    ref.read(taskProvider.notifier).addTask(
          _titleController.text.trim(),
          description: _descriptionController.text.trim().isEmpty
              ? null
              : _descriptionController.text.trim(),
          priority: _priority,
        );

    Navigator.pop(context);
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 16,
        right: 16,
        top: 16,
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            'Add New Task',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _titleController,
            autofocus: true,
            decoration: const InputDecoration(
              labelText: 'Task Title',
              hintText: 'What needs to be done?',
            ),
            textInputAction: TextInputAction.next,
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _descriptionController,
            decoration: const InputDecoration(
              labelText: 'Description (optional)',
              hintText: 'Add details...',
            ),
            maxLines: 2,
          ),
          const SizedBox(height: 16),
          Text(
            'Priority',
            style: Theme.of(context).textTheme.titleSmall,
          ),
          const SizedBox(height: 8),
          SegmentedButton<Priority>(
            segments: const [
              ButtonSegment(
                value: Priority.low,
                label: Text('Low'),
                icon: Icon(Icons.arrow_downward),
              ),
              ButtonSegment(
                value: Priority.medium,
                label: Text('Medium'),
                icon: Icon(Icons.remove),
              ),
              ButtonSegment(
                value: Priority.high,
                label: Text('High'),
                icon: Icon(Icons.arrow_upward),
              ),
            ],
            selected: {_priority},
            onSelectionChanged: (selected) {
              setState(() {
                _priority = selected.first;
              });
            },
          ),
          const SizedBox(height: 24),
          FilledButton(
            onPressed: _submit,
            child: const Text('Add Task'),
          ),
          const SizedBox(height: 16),
        ],
      ),
    );
  }
}

Navigation with Go Router

# pubspec.yaml
dependencies:
  go_router: ^13.0.0
// lib/router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'screens/home_screen.dart';
import 'screens/task_detail_screen.dart';
import 'screens/settings_screen.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/task/:id',
      builder: (context, state) {
        final taskId = state.pathParameters['id']!;
        return TaskDetailScreen(taskId: taskId);
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
);

// Usage in main.dart
MaterialApp.router(
  routerConfig: router,
  // ...
)

// Navigation
context.go('/task/123');
context.push('/settings');
context.pop();

API Integration

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

class ApiService {
  static const baseUrl = 'https://api.example.com';

  final http.Client _client;

  ApiService({http.Client? client}) : _client = client ?? http.Client();

  Future<T> _request<T>({
    required String method,
    required String path,
    Map<String, dynamic>? body,
    T Function(dynamic json)? fromJson,
  }) async {
    final uri = Uri.parse('$baseUrl$path');
    final headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    };

    http.Response response;

    switch (method) {
      case 'GET':
        response = await _client.get(uri, headers: headers);
        break;
      case 'POST':
        response = await _client.post(
          uri,
          headers: headers,
          body: body != null ? jsonEncode(body) : null,
        );
        break;
      case 'PUT':
        response = await _client.put(
          uri,
          headers: headers,
          body: body != null ? jsonEncode(body) : null,
        );
        break;
      case 'DELETE':
        response = await _client.delete(uri, headers: headers);
        break;
      default:
        throw Exception('Unsupported method: $method');
    }

    if (response.statusCode >= 200 && response.statusCode < 300) {
      if (fromJson != null) {
        return fromJson(jsonDecode(response.body));
      }
      return jsonDecode(response.body) as T;
    } else {
      throw ApiException(
        statusCode: response.statusCode,
        message: response.body,
      );
    }
  }

  Future<List<Task>> getTasks() async {
    return _request<List<Task>>(
      method: 'GET',
      path: '/tasks',
      fromJson: (json) =>
          (json as List).map((e) => Task.fromJson(e)).toList(),
    );
  }

  Future<Task> createTask(Task task) async {
    return _request<Task>(
      method: 'POST',
      path: '/tasks',
      body: task.toJson(),
      fromJson: (json) => Task.fromJson(json),
    );
  }
}

class ApiException implements Exception {
  final int statusCode;
  final String message;

  ApiException({required this.statusCode, required this.message});

  @override
  String toString() => 'ApiException: $statusCode - $message';
}

Production Build

# Android APK
flutter build apk --release

# Android App Bundle (for Play Store)
flutter build appbundle --release

# iOS (requires Mac)
flutter build ios --release

# Then archive in Xcode:
# open ios/Runner.xcworkspace
# Product > Archive

For secure builds and app signing, see our Mobile App Security Best Practices guide.

Common Mistakes to Avoid

Mistake Problem Solution
Not using const Unnecessary rebuilds Add const to widgets that don't change
setState in StatelessWidget Crash Convert to StatefulWidget or use state management
Unbounded lists in Column Layout errors Use ListView or add constraints
Missing dispose() Memory leaks Dispose controllers and subscriptions
Blocking UI thread Frozen app Use compute() for heavy operations

Frequently Asked Questions

Is Flutter good for beginners?

Yes. Dart is easy to learn, hot reload makes development fast, and Flutter's widget system is intuitive once you understand the basics. The learning curve is moderate — expect 2-4 weeks to build basic apps, 2-3 months for production quality.

Flutter vs React Native: Which is better?

Flutter has better performance and more consistent UI across platforms. React Native has a larger ecosystem and is easier if you know JavaScript. Choose Flutter for design-heavy apps; choose React Native if your team knows JavaScript/React.

Can Flutter be used for production apps?

Absolutely. Google Pay, BMW, Alibaba, eBay Motors, and many other production apps use Flutter. It's mature, stable, and actively maintained by Google with regular updates.

Is Dart hard to learn?

No. If you know any C-style language (JavaScript, Java, C#, Swift), Dart will feel familiar. Its syntax is clean, and null safety prevents common bugs. Most developers become productive in Dart within 1-2 weeks.

How do I choose between Provider, Riverpod, and BLoC?

Provider: Simple, good for small/medium apps. Riverpod: Modern, compile-safe, better testing — recommended for new projects. BLoC: Powerful but verbose, good for large teams with strict patterns. Start with Riverpod for most projects.

Ready to Build Your Flutter App?

You now have the foundation to build production Flutter apps. The key is practice — build real projects to solidify your skills.

If you need help building a Flutter app for your business, we create beautiful, high-performance apps that work on iOS, Android, and beyond.

Get in touch:


Related Articles

Tags:FlutterDartAndroidiOSMobile DevelopmentTutorialRiverpod

Need help with your project?

We've helped 534+ clients build successful apps. Let's discuss yours.

Ready to Build Your App?

534+ projects delivered • 4.9★ rating • 6+ years experience

Let's discuss your project — no obligations, just a straightforward conversation.