Flutter Complete Guide | Cross-Platform Apps with Dart

Flutter Complete Guide | Cross-Platform Apps with Dart

이 글의 핵심

Flutter is Google's UI toolkit for building natively compiled apps for mobile, web, and desktop from a single Dart codebase. This guide covers the Flutter widget model, state management, navigation, and production deployment.

What This Guide Covers

Flutter lets you write one Dart codebase that compiles to native iOS, Android, web, and desktop apps. This guide covers the widget system, state management patterns, navigation, HTTP, and deployment.

Real-world insight: A team shipped iOS and Android apps simultaneously in 8 weeks with Flutter — the consistent UI behavior across platforms eliminated an entire class of OS-specific bugs.


Setup

# Install Flutter SDK
# See flutter.dev/docs/get-started/install for your OS

# Verify installation
flutter doctor

# Create a new app
flutter create my_app
cd my_app
flutter run

1. Everything Is a Widget

In Flutter, UI is built by composing 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: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: const Center(
        child: Text('Hello, Flutter!', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

2. StatefulWidget

Use StatefulWidget when the widget needs to rebuild on data changes:

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  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.displayLarge),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

3. Layout Widgets

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

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    const Text('Left'),
    ElevatedButton(onPressed: () {}, child: const Text('Right')),
  ],
)

// Expanded fills available space
Row(
  children: [
    const Icon(Icons.search),
    Expanded(
      child: TextField(decoration: InputDecoration(hintText: 'Search...')),
    ),
  ],
)

// Stack (z-axis layering)
Stack(
  children: [
    Image.network('https://picsum.photos/400/300'),
    Positioned(
      bottom: 16,
      left: 16,
      child: Text('Caption', style: TextStyle(color: Colors.white, fontSize: 18)),
    ),
  ],
)

4. Lists

// ListView.builder (lazy, efficient for large lists)
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    final item = items[index];
    return ListTile(
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text(item.title),
      subtitle: Text(item.subtitle),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => Navigator.pushNamed(context, '/detail', arguments: item),
    );
  },
)

// GridView
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 12,
    mainAxisSpacing: 12,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) => ProductCard(product: products[index]),
)

5. Navigation

// Named routes
MaterialApp(
  routes: {
    '/': (context) => const HomeScreen(),
    '/detail': (context) => const DetailScreen(),
    '/settings': (context) => const SettingsScreen(),
  },
)

// Navigate
Navigator.pushNamed(context, '/detail', arguments: item)
Navigator.pop(context)

// GoRouter (recommended for complex apps)
flutter pub add go_router
import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
    GoRoute(
      path: '/posts/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return PostDetailScreen(id: id);
      },
    ),
  ],
);

// Navigate
context.go('/posts/123')
context.push('/posts/123')  // pushes onto stack
context.pop()

6. HTTP and API Calls

flutter pub add http
import 'dart:convert';
import 'package:http/http.dart' as http;

class Post {
  final int id;
  final String title;
  final String body;

  const 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']);
  }
}

Future<List<Post>> fetchPosts() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
  );

  if (response.statusCode == 200) {
    final List<dynamic> json = jsonDecode(response.body);
    return json.map((j) => Post.fromJson(j)).toList();
  } else {
    throw Exception('Failed to load posts');
  }
}

// Use with FutureBuilder
FutureBuilder<List<Post>>(
  future: fetchPosts(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }
    final posts = snapshot.data!;
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, i) => ListTile(title: Text(posts[i].title)),
    );
  },
)

7. State Management with Riverpod

flutter pub add flutter_riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

// Async provider (fetch data)
final postsProvider = FutureProvider<List<Post>>((ref) async {
  return fetchPosts();
});

// Main app setup
void main() {
  runApp(
    const ProviderScope(  // wrap app with ProviderScope
      child: MyApp(),
    ),
  );
}

// Use in widget (ConsumerWidget instead of StatelessWidget)
class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class PostListPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postsAsync = ref.watch(postsProvider);

    return postsAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, _) => Text('Error: $err'),
      data: (posts) => ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, i) => ListTile(title: Text(posts[i].title)),
      ),
    );
  }
}

8. Common Widgets Cheatsheet

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

// Image
Image.network('https://picsum.photos/200')
Image.asset('assets/images/logo.png')

// Icon
Icon(Icons.favorite, color: Colors.red, size: 32)

// Button variants
ElevatedButton(onPressed: () {}, child: Text('Elevated'))
TextButton(onPressed: () {}, child: Text('Text'))
OutlinedButton(onPressed: () {}, child: Text('Outlined'))
IconButton(icon: Icon(Icons.share), onPressed: () {})

// Input
TextField(
  controller: _controller,
  decoration: InputDecoration(
    labelText: 'Email',
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(),
  ),
)

// Card
Card(
  elevation: 2,
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  child: Padding(padding: EdgeInsets.all(16), child: /* content */),
)

// Padding / Margin
Padding(padding: EdgeInsets.all(16), child: /* content */)
Container(margin: EdgeInsets.symmetric(horizontal: 16), child: /* content */)

9. Building and Deployment

# Development
flutter run                 # default device
flutter run -d chrome       # web
flutter run -d macos        # macOS

# Build
flutter build apk           # Android APK
flutter build appbundle     # Android (Play Store)
flutter build ios           # iOS (requires macOS + Xcode)
flutter build web           # Web

# Release build
flutter build apk --release

For App Store / Play Store submission, use Fastlane or Codemagic CI/CD to automate signing and upload.


Key Takeaways

ConceptKey point
WidgetEverything is a widget — composable, immutable
StatelessWidgetNo internal state — pure rendering
StatefulWidgetInternal state with setState()
LayoutColumn, Row, Stack, Expanded, Padding
ListsListView.builder for efficiency
NavigationGoRouter for named routes and deep links
HTTPhttp package + FutureBuilder
StateRiverpod (recommended) or Provider

Flutter’s “everything is a widget” model feels unusual at first but becomes natural quickly. The key insight: compose small widgets into larger ones, use StatefulWidget only where you need state, and use Riverpod to lift shared state out of widgets.