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
| Concept | Key point |
|---|---|
| Widget | Everything is a widget — composable, immutable |
| StatelessWidget | No internal state — pure rendering |
| StatefulWidget | Internal state with setState() |
| Layout | Column, Row, Stack, Expanded, Padding |
| Lists | ListView.builder for efficiency |
| Navigation | GoRouter for named routes and deep links |
| HTTP | http package + FutureBuilder |
| State | Riverpod (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.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Build iOS, Android, and web apps with Flutter. Covers widgets, state management, navigation, HTTP requests, and deployme… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [React Native Complete Guide | Mobile Apps with JavaScript](/en/blog/react-native-complete-guide-en/
이 글에서 다루는 키워드 (관련 검색어)
Flutter, Dart, Mobile, iOS, Android, Cross-platform 등으로 검색하시면 이 글이 도움이 됩니다.