오늘은 플러터 코드랩의 'First Flutter app'의 구조를 간단하게 파악하는 시간을 가져보았다. 아래는 샘플 코드의 최종 버전이다. 환경 설정부터 아래 코드까지 구성하는 과정은 코드랩 페이지를 참고하면 된다.
First Flutter app 코드 설명
두 개의 주요 화면(랜덤 단어 생성기와 즐겨찾기 목록)을 포함하고 있으며, provider 패키지를 사용하여 상태 관리를 수행한다. 또한, 이 앱은 반응형 UI를 사용하기 때문에 화면의 크기에 따라 UI가 달라진다.
First Flutter app 결과 화면
Home 화면
First Flutter app 코드 및 주석
// 패키지와 라이브러리를 가져옴
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
// 앱의 최상위 위젯
class MyApp extends StatelessWidget {
const MyApp({
super.key
});
@override
Widget build(BuildContext context) {
// 상태 관리를 위해 ChangeNotifierProvider 사용
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true, // Material 3 디자인 사용
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// 앱의 상태를 관리하는 클래스
class MyAppState extends ChangeNotifier {
var current = WordPair.random(); // 현재 표시되는 단어 쌍
// 새로운 랜덤 단어 쌍 생성
void getNext() {
current = WordPair.random();
notifyListeners(); // 상태 변경 알림
}
var favorites = <WordPair>[]; // 즐겨찾기 목록
// 즐겨찾기 상태 토글
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners(); // 상태 변경 알림
}
}
// 홈 페이지 위젯
class MyHomePage extends StatefulWidget {
@override
State < MyHomePage > createState() => _MyHomePageState();
}
class _MyHomePageState extends State < MyHomePage > {
var selectedIndex = 0; // 선택된 탭 인덱스 (0: Home, 1: Favorites)
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = FavoritePage();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// 반응형 UI를 위한 레이아웃 빌더
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
// 내비게이션 레일
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600,
// 화면 너비가 600 이상일 경우, 확장 모드
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
// 선택된 인덱스를 업데이트
});
},
),
),
// 화면의 나머지 부분을 선택된 페이지로 채움
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// 즐겨찾기 페이지
class FavoritePage extends StatelessWidget {
const FavoritePage({
super.key,
});
@override
Widget build(BuildContext context) {
var appState = context.watch < MyAppState > (); // 상태 가져옴
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(leading: Icon(Icons.favorite), title: Text(pair.asLowerCase))
],
);
}
}
// 단어 쌍 생성기 페이지
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch < MyAppState > (); // 상태 가져옴
var pair = appState.current;
// 즐겨찾기 여부에 따라 아이콘 설정
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
// 즐겨찾기에 추가되면 '하트' 아이콘
} else {
icon = Icons.favorite_border;
// 즐겨찾기에 없으면 '하트 빈 테두리' 아이콘
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair), // 큰 카드에 단어 쌍 표시
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// '좋아요' 버튼
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
// 좋아요 상태 토글
},
icon: Icon(icon), // 현재 상태에 맞는 아이콘
label: Text('Like'),
),
SizedBox(width: 10),
// '다음' 버튼
ElevatedButton(
onPressed: () {
appState.getNext(); // 다음 단어 쌍 가져옴
},
child: Text('Next'),
),
],
),
],
),
);
}
}
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
}
First Flutter app 코드 접근 방식
1. 전체 구조 확인
먼저, 전체 코드의 흐름과 구조를 빠르게 살펴보면, `main()` 함수가 `MyApp` 위젯을 실행시키는 것을 알 수 있다. 그리고 `MyApp`이 `MyHomePage`을 실행시키는 구조이다.
2. 클래스 및 함수 이해
각 클래스와 함수의 역할을 이해하기 위해 주석, 변수명, 함수명 등을 살펴본다. 예를 들어, `GeneratorPage`는 이름에서 알 수 있듯이 단어를 생성하는 페이지이다.
3. 상태 관리
`MyAppState` 클래스가 앱의 중요한 상태를 관리하고 있다는 것을 알 수 있다. 이 클래스에는 현재 단어 쌍(`current`)과 즐겨찾기 목록(`favorites`) 등의 상태가 있다.
4. 위젯 트리 분석
각 위젯의 `build` 메서드를 살펴보며, 어떤 위젯이 어떤 자식 위젯을 가지는지 확인한다. 이를 통해 UI의 계층 구조를 이해하게 된다.
5. 이벤트 처리
어떻게 사용자의 입력을 처리하는지 확인한다. 여기서는 `ElevatedButton` 위젯에 연결된 `onPressed` 핸들러를 통해 앱 상태가 어떻게 변경되는지 이해한다.
6. 상태 변화
`notifyListeners()` 함수 호출이 어떻게 위젯 트리를 다시 렌더링하는지 이해한다.
7. 의존성 확인
`import` 구문을 통해 외부 라이브러리와의 의존성을 확인한다. 이 예제에서는 `english_words`와 `provider` 패키지가 사용되고 있다.