1. 소개
Flutter는 하나의 코드베이스를 사용해 모바일, 웹, 데스크톱을 대상으로 애플리케이션을 빌드하는 Google의 UI 도구 모음입니다. 이 Codelab에서는 다음과 같은 Flutter 애플리케이션을 빌드합니다.
이 애플리케이션에서는 'newstay'나 'lightstream', 'mainbrake', 'graypine'과 같은 멋진 이름을 생성합니다. 사용자는 다음 이름을 요청하거나 현재 이름을 즐겨찾기에 추가하거나 별도의 페이지에서 즐겨찾기에 추가한 이름 목록을 검토할 수 있습니다. 앱은 다양한 화면 크기에 반응합니다.
학습할 내용
- Flutter 작동 방식의 기본사항
- Flutter에서 레이아웃 만들기
- 버튼 누르기 등 사용자 상호작용을 앱 동작에 연결
- Flutter 코드 체계적으로 유지
- 앱을 반응형으로 만들기(다양한 화면에 맞게)
- 앱의 일관된 디자인과 분위기 달성
바로 흥미로운 부분으로 들어갈 수 있도록 먼저 기본 스캐폴드부터 시작합니다.
필립이 전체 Codelab을 안내합니다.
실습을 시작하려면 Next를 클릭하세요.
2. Flutter 환경 설정
편집기
이 Codelab을 최대한 간단하게 만들기 위해 Google에서는 개발자가 Visual Studio Code(VS Code)를 개발 환경으로 사용한다고 가정합니다. 이 편집기는 무료이며 모든 주요 플랫폼에서 작동합니다.
물론 Android 스튜디오, 기타 IntelliJ IDE, Emacs, Vim, Notepad++ 등 원하는 다른 편집기를 사용해도 됩니다. 모두 Flutter와 호환됩니다.
이 Codelab에 VS Code 사용을 권장하는 이유는 안내에 VS Code 관련 단축키가 기본적으로 설정되어 있기 때문입니다. 'X를 실행하려면 편집기에서 적절한 작업을 하세요'라고 하는 것보다는 '여기를 클릭하세요'나 '이 키를 누르세요'라고 하는 것이 더 쉽습니다.
개발 타겟 선택
Flutter는 다중 플랫폼 도구 모음입니다. 앱이 다음 운영체제 어디서든 실행될 수 있습니다.
- iOS
- Android
- Windows
- macOS
- Linux
- 웹
그러나 단일 운영체제를 선택하고 주로 그 운영체제를 대상으로 개발하는 것이 일반적입니다. 이를 '개발 타겟'이라고 합니다. 즉, 개발하는 동안 앱을 실행할 운영체제입니다.
예를 들어 Windows 노트북을 사용하여 Flutter 앱을 개발한다고 가정해 보겠습니다. Android를 개발 타겟으로 선택하면 일반적으로 USB 케이블을 통해 Android 기기를 Windows 노트북에 연결하고 개발 중인 앱은 연결된 Android 기기에서 실행됩니다. 그러나 개발 타겟으로 Windows를 선택할 수도 있습니다. 즉, 개발 중인 앱이 편집기와 함께 Windows 앱으로 실행됩니다.
개발 타겟으로 웹을 선택하고 싶을 수 있습니다. 웹을 선택하면 Flutter의 가장 유용한 개발 기능 중 하나(스테이트풀(Stateful) 핫 리로드)를 사용할 수 없다는 단점이 있습니다. Flutter는 웹 애플리케이션을 핫 리로드할 수 없습니다.
이제 선택하세요. 나중에 언제든지 다른 운영체제에서 앱을 실행할 수 있습니다. 개발 타겟을 분명하게 정해 두면 다음 단계를 원활하게 진행할 수 있습니다.
Flutter 설치
Flutter SDK 설치 방법에 관한 가장 최신 안내는 docs.flutter.dev에서 항상 확인할 수 있습니다.
Flutter 웹사이트에서는 SDK 자체의 설치뿐 아니라 개발 타겟 관련 도구와 편집기 플러그인에 관해서도 안내합니다. 이 Codelab의 경우 다음 항목만 설치하면 됩니다.
- Flutter SDK
- Flutter 플러그인이 있는 Visual Studio Code
- 선택한 개발 타겟에 필요한 소프트웨어(예: Windows를 타겟팅하는 Visual Studio, macOS를 타겟팅하는 Xcode)
다음 섹션에서는 첫 번째 Flutter 프로젝트를 만들어 봅니다.
문제가 있는 경우 문제 해결에 다음과 같은 질문과 답변(StackOverflow에서 제공)이 도움이 될 수 있습니다.
자주 묻는 질문(FAQ)
- Flutter SDK의 경로는 어떻게 찾을 수 있나요?
- Flutter 명령어를 찾을 수 없으면 어떻게 해야 하나요?
- '시작 잠금을 해제하기 위해 다른 Flutter 명령어를 기다리는 중' 문제를 해결하려면 어떻게 해야 하나요?
- Flutter에 Android SDK 설치 위치를 알리려면 어떻게 해야 하나요?
flutter doctor --android-licenses
를 실행할 때 Java 오류를 처리하려면 어떻게 해야 하나요?- 찾을 수 없는 Android
sdkmanager
도구는 어떻게 처리해야 하나요? - '
cmdline-tools
구성요소가 누락됨' 오류는 어떻게 처리해야 하나요? - Apple Silicon(M1)에서 CocoaPods를 실행하려면 어떻게 해야 하나요?
- VS Code에서 저장 시 자동 형식 지정을 사용 중지하려면 어떻게 해야 하나요?
3. 프로젝트 만들기
첫 번째 Flutter 프로젝트 만들기
Visual Studio Code를 실행하고 명령어 팔레트를 엽니다(F1
또는 Ctrl+Shift+P
또는 Shift+Cmd+P
사용). 'flutter new'를 입력합니다. Flutter: New Project 명령어를 선택합니다.
Application을 선택하고 프로젝트를 만들 폴더를 선택합니다. 홈 디렉터리이거나 C:\src\
같은 것일 수 있습니다.
이제 프로젝트의 이름을 지정합니다. namer_app
또는 my_awesome_namer
를 예로 들 수 있습니다.
이제 Flutter에서 프로젝트 폴더를 생성하고 VS Code에서 이 폴더를 엽니다.
이제 앱의 기본 스캐폴드로 3개 파일의 콘텐츠를 덮어씁니다.
초기 앱 복사 및 붙여넣기
VS Code의 왼쪽 창에서 Explorer가 선택되어 있는지 확인하고 pubspec.yaml
파일을 엽니다.
이 파일의 콘텐츠를 다음으로 바꿉니다.
pubspec.yaml
name: namer_app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: '>=2.19.4 <4.0.0'
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
pubspec.yaml
파일은 앱에 관한 기본 정보(예: 현재 버전, 종속 항목, 함께 제공될 애셋)를 지정합니다.
프로젝트에서 또 다른 구성 파일 analysis_options.yaml
을 엽니다.
파일의 콘텐츠를 다음으로 바꿉니다.
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
prefer_const_constructors: false
prefer_final_fields: false
use_key_in_widget_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_const_constructors_in_immutables: false
avoid_print: false
이 파일은 코드를 분석할 때 Flutter의 엄격성 정도를 결정합니다. 이번이 Flutter를 처음 사용하는 것이므로 분석 도구에 쉬엄쉬엄하자고 지시하는 것입니다. 이는 나중에 언제든지 조정할 수 있습니다. 사실 실제 프로덕션 앱을 게시할 때가 가까워지면 분명 이보다는 더 엄격하게 분석 도구를 만들게 됩니다.
이제 lib/
디렉터리 아래의 main.dart
파일을 엽니다.
이 파일의 콘텐츠를 다음으로 바꿉니다.
lib/main.dart
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) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [
Text('A random idea:'),
Text(appState.current.asLowerCase),
],
),
);
}
}
이 50줄의 코드가 지금까지 전체 앱입니다.
다음 섹션에서는 디버그 모드로 애플리케이션을 실행하고 개발을 시작합니다.
4. 버튼 추가
이 단계에서는 Next 버튼을 추가하여 새 단어 쌍을 생성합니다.
앱 실행
먼저 lib/main.dart
를 열고 대상 기기가 선택되어 있는지 확인합니다. VS Code 오른쪽 하단에 현재 대상 기기를 보여주는 버튼이 있습니다. 클릭하여 변경합니다.
lib/main.dart
가 열려 있는 동안 VS Code 오른쪽 상단에서 'play' 버튼을 찾아 클릭합니다.
1분 정도 지나면 앱이 디버그 모드로 실행됩니다. 아직은 간단한 앱입니다.
첫 번째 핫 리로드
lib/main.dart
하단에서 첫 번째 Text
객체의 문자열에 무언가를 추가하고 Ctrl+S
또는 Cmd+S
를 사용하여 파일을 저장합니다. 예를 들면 다음과 같습니다.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
앱이 즉각적으로 변경되지만 random 단어는 동일하게 유지됩니다. 이는 Flutter의 유명한 스테이트풀(Stateful) 핫 리로드가 작동하는 것입니다. 핫 리로드는 소스 파일에 변경사항을 저장할 때 트리거됩니다.
자주 묻는 질문(FAQ)
- 핫 리로드가 VSCode에서 작동하지 않으면 어떻게 되나요?
- VSCode에서 핫 리로드를 사용하려면 'r'을 눌러야 하나요?
- 핫 리로드는 웹에서 작동하나요?
- 'Debug' 배너를 삭제하려면 어떻게 해야 하나요?
버튼 추가
이제 두 번째 Text
인스턴스 바로 아래 Column
하단에 버튼을 추가합니다.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
변경사항을 저장하면 앱이 다시 업데이트됩니다. 버튼이 표시되고 이 버튼을 클릭하면 VS Code의 Debug Console에 button pressed! 메시지가 표시됩니다.
Flutter 5분 집중 과정
Debug Console을 보는 것이 재미있지만 버튼이 좀 더 의미 있는 작업을 했으면 합니다. 이 작업을 하기 전에 먼저 lib/main.dart
의 코드를 자세히 살펴보고 작동 방식을 파악하세요.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
파일 최상단에는 main()
함수가 있습니다. 현재 형식으로는 MyApp
에서 정의된 앱을 실행하라고 Flutter에 지시할 뿐입니다.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
MyApp
클래스는 StatelessWidget
을 확장합니다. 위젯은 모든 Flutter 앱을 빌드하는 데 사용되는 요소입니다. 앱 자체도 위젯인 것을 확인할 수 있습니다.
MyApp
의 코드는 전체 앱을 설정합니다. 앱 전체 상태를 생성하고(나중에 자세히 설명) 앱의 이름을 지정하고 시각적 테마를 정의하고 '홈' 위젯(앱의 시작점)을 설정합니다.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
MyAppState
클래스는 앱의 상태를 정의합니다. 이번이 처음으로 Flutter를 사용하는 것이므로 이 Codelab에서는 코드를 간단하고 명확하게 유지합니다. Flutter에는 앱 상태를 관리하는 강력한 방법이 여러 가지 있습니다. 설명하기 가장 쉬운 것 중 하나는 이 앱에서 사용하는 접근 방식인 ChangeNotifier
입니다.
MyAppState
는 앱이 작동하는 데 필요한 데이터를 정의합니다. 지금은 현재 임의의 단어 쌍이 있는 단일 변수만 포함되어 있습니다. 나중에 더 추가합니다.- 상태 클래스는
ChangeNotifier
를 확장합니다. 즉, 자체 변경사항에 관해 다른 항목에 알릴 수 있습니다. 예를 들어 현재 단어 쌍이 변경되면 앱의 일부 위젯이 알아야 합니다. - 상태가 만들어지고
ChangeNotifierProvider
를 사용하여 전체 앱에 제공됩니다(위의MyApp
코드 참고). 이렇게 하면 앱의 위젯이 상태를 알 수 있습니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
이제 MyHomePage
가 있습니다. 이 위젯은 이미 수정했습니다. 아래 번호가 지정된 각 줄은 위 코드의 줄 번호 주석에 매핑됩니다.
- 모든 위젯은 위젯이 항상 최신 상태로 유지되도록 위젯의 상황이 변경될 때마다 자동으로 호출되는
build()
메서드를 정의합니다. MyHomePage
는watch
메서드를 사용하여 앱의 현재 상태에 관한 변경사항을 추적합니다.- 모든
build
메서드는 위젯 또는 중첩된 위젯 트리(좀 더 일반적임)를 반환해야 합니다. 여기서 최상위 위젯은Scaffold
입니다. 이 Codelab에서는Scaffold
를 사용하지 않지만 유용한 위젯이며 대부분의 실제 Flutter 앱에서 찾을 수 있습니다. Column
은 Flutter에서 가장 기본적인 레이아웃 위젯 중 하나입니다. 하위 요소를 원하는 대로 사용하고 이를 위에서 아래로 열에 배치합니다. 기본적으로 열은 시각적으로 하위 요소를 상단에 배치합니다. 열이 중앙에 위치하도록 이를 곧 변경합니다.- 첫 번째 단계에서 이
Text
위젯을 변경했습니다. - 이 두 번째
Text
위젯은appState
를 사용하고 해당 클래스의 유일한 멤버인current
(즉,WordPair
)에 액세스합니다.WordPair
는asPascalCase
또는asSnakeCase
등 여러 유용한 getter를 제공합니다. 여기서는asLowerCase
를 사용하지만 대안 중 하나가 더 좋다면 지금 변경해도 됩니다. - Flutter 코드에서는 후행 쉼표를 많이 사용합니다. 이 특정 쉼표는 여기 없어도 됩니다.
children
이 이 특정Column
매개변수 목록의 마지막 멤버이자 유일한 멤버이기 때문입니다. 그러나 일반적으로 후행 쉼표를 사용하는 것이 좋습니다. 멤버를 더 추가하는 작업이 쉬워지고 Dart의 자동 형식 지정 도구에서 줄바꿈을 추가하도록 힌트 역할을 합니다. 자세한 내용은 코드 형식 지정을 참고하세요.
이제 버튼을 상태에 연결해 봅니다.
첫 번째 동작
MyAppState
로 스크롤하여 getNext
메서드를 추가합니다.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
새 getNext()
메서드는 임의의 새 WordPair
를 current
에 재할당합니다. 또한 MyAppState
를 보고 있는 사람에게 알림을 보내는 notifyListeners()
(ChangeNotifier)
의 메서드)를 호출합니다.
이제 남은 작업은 버튼의 콜백에서 getNext
메서드를 호출하는 것입니다.
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
저장하고 앱을 실행해 봅니다. Next 버튼을 누를 때마다 임의의 새 단어 쌍이 생성되어야 합니다.
다음 섹션에서는 사용자 인터페이스를 더 멋지게 만듭니다.
5. 더 멋진 앱 만들기
지금 앱의 모습은 다음과 같습니다.
잘 보이지 않습니다. 앱의 핵심인 임의로 생성된 단어 쌍이 더 잘 보여야 합니다. 사용자가 이 앱을 사용하는 주된 이유이기 때문입니다. 앱 콘텐츠도 너무 한쪽으로 치우쳐 있고 전체 앱도 흰색과 검은색으로 이루어져 따분합니다.
이 섹션에서는 앱의 디자인을 작업하여 이러한 문제를 해결합니다. 이 섹션의 최종 목표는 다음과 같습니다.
위젯 추출
현재 단어 쌍을 표시하는 줄은 Text(appState.current.asLowerCase)
와 같습니다. 좀 더 복잡하게 변경하려면 이 줄을 별도의 위젯으로 추출하는 것이 좋습니다. UI의 개별 논리적 부분을 위한 별도의 위젯을 갖는 것은 Flutter에서 복잡성을 관리하는 중요한 방법입니다.
Flutter는 위젯 추출을 위한 리팩터링 도우미를 제공하지만 이를 사용하기 전에 추출되는 줄이 필요한 항목에만 액세스하는지 확인하세요. 지금은 이 줄이 appState
에 액세스하지만 실제로는 현재 단어 쌍이 무엇인지만 알면 됩니다.
따라서 다음과 같이 MyHomePage
위젯을 다시 작성합니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
좋습니다. Text
위젯이 더 이상 전체 appState
를 참조하지 않습니다.
이제 Refactor 메뉴를 불러옵니다. VS Code에서는 두 가지 방법 중 하나로 이를 실행합니다.
- 리팩터링하려는 코드 부분(여기서는
Text
)을 마우스 오른쪽 버튼으로 클릭하고 드롭다운 메뉴에서 Refactor...를 선택합니다.
또는
- 리팩터링하려는 코드 부분(여기서는
Text
)으로 커서를 이동하고Ctrl+.
(Win/Linux) 또는Cmd+.
(Mac)를 누릅니다.
Refactor 메뉴에서 Extract Widget을 선택합니다. 이름(예: BigCard)을 할당하고 Enter
를 클릭합니다.
그러면 자동으로 현재 파일의 끝부분에 새 클래스 BigCard
가 생성됩니다. 이 클래스는 다음과 같이 표시됩니다.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
이 리팩터링을 실행하는 동안에도 앱은 계속 작동합니다.
카드 추가
이제 이 새 위젯을 이 섹션을 시작할 때 목표로 한 굵게 표시된 UI로 만들어 보겠습니다.
BigCard
클래스와 클래스 내에 있는 build()
메서드를 찾습니다. 이전처럼 Text
위젯에서 Refactor 메뉴를 불러옵니다. 하지만 이번에는 위젯을 추출하지 않습니다.
대신 Wrap with Padding을 선택합니다. 이렇게 하면 Text
위젯 주위에 Padding
이라는 새 상위 위젯이 만들어집니다. 저장하면 임의의 단어 주위에 이미 공간이 많이 생긴 것을 확인할 수 있습니다.
패딩을 기본값 8.0
에서 늘립니다. 예를 들어 20
정도 값을 사용하여 패딩을 널찍하게 만듭니다.
이제 한 단계 올라갑니다. Padding
위젯에 커서를 놓고 Refactor 메뉴를 불러온 다음 Wrap with widget...을 선택합니다.
이렇게 하면 상위 위젯을 지정할 수 있습니다. 'Card'를 입력하고 Enter를 누릅니다.
이렇게 하면 Padding
위젯과 Text
위젯이 Card
위젯으로 래핑됩니다.
테마와 스타일
카드를 좀 더 눈에 띄게 만들려면 좀 더 강렬한 색상으로 칠하세요. 항상 색 구성표는 일관되게 유지하는 것이 좋으므로 앱의 Theme
을 사용하여 색상을 선택합니다.
BigCard
의 build()
메서드를 다음과 같이 변경합니다.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
새로 추가된 이 두 줄은 많은 일을 합니다.
- 먼저 코드는
Theme.of(context)
로 앱의 현재 테마를 요청합니다. - 그런 다음, 코드는 테마의
colorScheme
속성과 동일하도록 카드의 색상을 정의합니다. 색 구성표에는 여러 색상이 포함되어 있으며primary
가 앱을 정의하는 가장 두드러진 색상입니다.
카드가 이제 앱의 기본 색상으로 칠해졌습니다.
MyApp
으로 스크롤하고 거기에서 ColorScheme
의 시드 색상을 변경하여 이 색상과 전체 앱의 색 구성표를 변경할 수 있습니다.
색상이 매끄럽게 애니메이션 처리됩니다. 이를 암시적 애니메이션이라고 합니다. 많은 Flutter 위젯은 값 간에 부드럽게 보간하므로 UI가 상태 간에 '건너뛰지' 않습니다.
카드 아래 돌출 버튼도 색상이 변경됩니다. 이는 하드 코딩 값이 아닌 앱 전체 Theme
을 사용하면 얻게 되는 이점입니다.
TextTheme
카드에는 여전히 문제가 있습니다. 텍스트가 너무 작아 색상을 알아보기 힘듭니다. 이 문제를 해결하려면 BigCard
의 build()
메서드를 다음과 같이 변경합니다.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
이 변경사항에는 다음이 포함됩니다.
theme.textTheme,
을 사용하여 앱의 글꼴 테마에 액세스합니다. 이 클래스에는bodyMedium
(중간 크기의 표준 텍스트용) 또는caption
(이미지 설명용),headlineLarge
(큰 헤드라인용) 등의 멤버가 포함되어 있습니다.displayMedium
속성은 디스플레이 텍스트를 위한 큰 스타일입니다. 여기서 디스플레이라는 단어는 디스플레이 서체와 같은 인쇄상의 의미로 사용됩니다.displayMedium
문서에는 '디스플레이 스타일은 짧고 중요한 텍스트용으로 예약되어 있습니다'(여기 사용 사례와 정확히 일치함)라고 나옵니다.- 테마의
displayMedium
속성은 이론적으로null
일 수 있습니다. 이 앱을 작성하는 데 사용하는 Dart라는 프로그래밍 언어는 null에 안전하므로null
이 될 수 있는 객체의 메서드를 개발자가 호출할 수 없습니다. 하지만 이 경우!
연산자('bang 연산자')를 사용하여 개발자가 잘 알고 하는 작업임을 Dart에 알릴 수 있습니다.displayMedium
은 이 경우 null이 아닌 것이 분명하지만 그 이유에 관한 내용은 이 Codelab에서 다루지 않습니다. displayMedium
에서copyWith()
를 호출하면 정의된 변경사항이 포함된 텍스트 스타일의 사본이 반환됩니다. 여기서는 텍스트의 색상만 변경합니다.- 새로운 색상을 가져오려면 앱의 테마에 다시 액세스해야 합니다. 색 구성표의
onPrimary
속성은 앱의 기본 색상으로 사용하기 적합한 색상을 정의합니다.
이제 앱이 다음과 같이 표시됩니다.
원한다면 카드를 더 변경해 보세요. 다음은 참고할 수 있는 팁입니다.
copyWith()
를 사용하면 텍스트 스타일에 관해 색상만이 아니라 여러 가지를 변경할 수 있습니다. 변경할 수 있는 전체 속성 목록을 가져오려면copyWith()
의 괄호 안 아무 데나 커서를 두고Ctrl+Shift+Space
(Win/Linux) 또는Cmd+Shift+Space
(Mac)를 누릅니다.- 마찬가지로
Card
위젯에 관해서도 여러 가지를 더 변경할 수 있습니다. 예를 들어elevation
매개변수의 값을 늘려 카드의 그림자를 확대할 수 있습니다. - 색상을 여러 가지로 실험해 봅니다.
theme.colorScheme.primary
외에도.secondary
,.surface
등 다양하게 많습니다. 이러한 모든 색상에는 상응하는onPrimary
가 있습니다.
접근성 개선
Flutter는 기본적으로 앱의 접근성을 개선합니다. 예를 들어 모든 Flutter 앱은 TalkBack, VoiceOver와 같은 스크린 리더에 앱의 모든 텍스트와 대화형 요소를 올바르게 표시합니다.
하지만 작업이 필요한 경우도 있습니다. 이 앱의 경우 생성된 일부 단어 쌍을 스크린 리더에서 잘 발음하지 못할 수 있습니다. 사람은 cheaphead를 구성하는 두 단어를 쉽게 식별할 수 있지만 스크린 리더는 가운데 ph를 f로 발음할 수 있습니다.
이 문제는 pair.asLowerCase
를 "${pair.first} ${pair.second}"
로 바꾸면 간단하게 해결됩니다. 후자는 문자열 보간 유형을 사용하여 pair
에 포함된 두 단어에서 문자열(예: "cheap head"
)을 만듭니다. 복합어 대신 개별 두 단어를 사용하면 스크린 리더에서 두 단어를 적절히 식별할 수 있고 시각장애인 사용자에게 더 나은 환경을 제공할 수 있습니다.
그러나 pair.asLowerCase
는 시각적으로 단순하게 유지하는 것이 좋습니다. Text
의 semanticsLabel
속성을 사용하여 텍스트 위젯의 시각적 콘텐츠를 스크린 리더에 더 적합한 시맨틱 콘텐츠로 재정의합니다.
lib/main.dart
// ...
@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),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
이제 스크린 리더가 생성된 각 단어 쌍을 올바르게 발음하지만 UI는 동일하게 유지됩니다. 기기에서 스크린 리더를 사용하여 실제로 이를 시도해 보세요.
UI 중앙 배치
이제 임의의 단어 쌍이 시각적으로 충분히 잘 표시되므로 이를 앱의 창/화면 중앙에 배치해 보겠습니다.
먼저 BigCard
는 Column
의 일부라는 점을 고려합니다. 기본적으로 열은 하위 요소를 상단으로 일괄 처리하지만 이는 쉽게 재정의할 수 있습니다. MyHomePage
의 build()
메서드로 가서 다음과 같이 변경합니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
이렇게 하면 기본(세로) 축을 따라 Column
내 하위 요소가 중앙에 배치됩니다.
하위 요소는 이미 열의 교차 축을 따라 중앙에 배치되어 있습니다(즉, 이미 가로로 중앙에 배치되어 있음). 그러나 Column
자체는 Scaffold
내에서 중앙에 배치되어 있지 않습니다. Widget Inspector를 사용하여 이를 확인할 수 있습니다.
Widget Inspector 자체는 이 Codelab의 범위에 포함되지 않지만 Column
이 강조 표시될 때 앱의 전체 너비를 차지하지 않는 것을 확인할 수 있습니다. 하위 요소에 필요한 가로 공간만큼만 차지합니다.
열 자체만 중앙에 배치하면 됩니다. Column
위에 커서를 두고 Refactor 메뉴를 불러와(Ctrl+.
또는 Cmd+.
사용) Wrap with Center를 선택합니다.
이제 앱이 다음과 같이 표시됩니다.
원한다면 조금 더 조정할 수 있습니다.
BigCard
위의Text
위젯을 삭제할 수 있습니다. 설명 텍스트('A random AWESOME idea:')가 없어도 UI를 이해할 수 있으므로 이 텍스트가 더 이상 필요하지 않다고 주장할 수 있습니다. 삭제하면 더 깔끔하기도 합니다.BigCard
와ElevatedButton
사이에SizedBox(height: 10)
위젯을 추가할 수도 있습니다. 이렇게 하면 두 위젯을 조금 더 분명하게 구분할 수 있습니다.SizedBox
위젯은 공간만 차지하고 자체적으로는 아무것도 렌더링하지 않습니다. 시각적 '간격'을 만들 때 흔히 사용됩니다.
선택적 변경사항을 적용하면 MyHomePage
에는 다음 코드가 포함됩니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
앱은 다음과 같이 표시됩니다.
다음 섹션에서는 생성된 단어를 즐겨찾기(또는 '좋아요')에 추가하는 기능을 추가합니다.
6. 기능 추가
앱이 작동하면서 때로는 흥미로운 단어 쌍을 제공하기도 합니다. 그러나 사용자가 Next를 클릭할 때마다 각 단어 쌍이 영원히 사라집니다. 따라서 최고의 추천 단어를 '기억'하는 방법(예: '좋아요' 버튼)이 있으면 더 좋습니다.
비즈니스 로직 추가
MyAppState
로 스크롤하여 다음 코드를 추가합니다.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
변경사항을 검토합니다.
MyAppState
에 새 속성favorites
를 추가했습니다. 이 속성은 빈 목록([]
)으로 초기화됩니다.- 또한 제네릭을 사용하여 목록에
<WordPair>[]
단어 쌍만 포함될 수 있다고 지정했습니다. 이렇게 하면 앱이 더 강력해집니다. Dart는 개발자가WordPair
외 다른 것을 추가하려고 하면 앱을 실행하는 것조차 거부합니다. 결국 숨겨져 있는 원치 않는 객체(예:null
)가 있을 수 없음을 알고favorites
목록을 사용할 수 있습니다.
- 새 메서드
toggleFavorite()
도 추가했습니다. 이 메서드는 즐겨찾기 목록에서 현재 단어 쌍을 삭제하거나(이미 있는 경우) 목록에 추가합니다(아직 없는 경우). 두 경우 모두 코드는 이후notifyListeners();
를 호출합니다.
버튼 추가
'비즈니스 로직'을 처리했으므로 이제 사용자 인터페이스를 다시 작업해 보겠습니다. 'Like' 버튼을 'Next' 버튼 왼쪽에 배치하려면 Row
를 사용해야 합니다. Row
위젯은 앞서 살펴본 Column
에 가로로 대응하는 것입니다.
먼저 기존 버튼을 Row
에 래핑합니다. MyHomePage
의 build()
메서드로 가서 ElevatedButton
에 커서를 두고 Ctrl+.
또는 Cmd+.
로 Refactor 메뉴를 불러온 다음 Wrap with Row를 선택합니다.
저장하면 Row
가 Column
과 유사하게 작동하는 것을 알 수 있습니다. 기본적으로 하위 요소를 왼쪽으로 일괄 처리합니다. Column
은 하위 요소를 상단으로 일괄 처리했습니다. 이를 해결하려면 이전과 같은 접근 방식을 사용하면 되지만 mainAxisAlignment
와 함께 사용합니다. 그러나 학습 목적으로는 mainAxisSize
를 사용하세요. 사용 가능한 모든 가로 공간을 차지하지 말라고 Row
에 지시합니다.
다음과 같이 변경합니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
UI가 이전 위치로 돌아갑니다.
이제 Like 버튼을 추가하고 toggleFavorite()
에 연결합니다. 도전해 보고 싶다면 먼저 아래 코드 블록을 보지 않고 직접 이를 시도해 보세요.
아래와 정확히 동일하게 실행하지 않아도 괜찮습니다. 진짜 어려운 도전을 원하는 경우가 아니라면 사실 하트 아이콘은 신경 쓰지 않아도 됩니다.
또한 실패해도 괜찮습니다. 이번이 처음 Flutter를 사용해 보는 것이니까요.
다음은 MyHomePage
에 두 번째 버튼을 추가하는 한 가지 방법입니다. 이번에는 ElevatedButton.icon()
생성자를 사용하여 아이콘이 있는 버튼을 만듭니다. build
메서드 상단에서 현재 단어 쌍이 이미 즐겨찾기에 있는지에 따라 적절한 아이콘을 선택하세요. 또한 SizedBox
를 다시 사용하여 두 버튼을 약간 떨어뜨립니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
앱이 다음과 같이 표시됩니다.
안타깝게도 사용자는 즐겨찾기를 확인할 수 없습니다. 이제 앱에 완전히 별도의 화면을 추가해야 합니다. 다음 섹션에서 만나요.
7. 탐색 레일 추가
대부분의 앱은 단일 화면에 모든 내용을 맞출 수 없습니다. 이 특정 앱이라면 그럴 수 있겠지만 학습을 위해 사용자의 즐겨찾기를 위한 별도의 화면을 만들어 보겠습니다. 두 화면 간에 전환하려면 첫 번째 StatefulWidget
을 구현합니다.
이 단계의 요점을 최대한 빨리 알아보기 위해 MyHomePage
를 별도의 위젯 2개로 분할합니다.
MyHomePage
를 모두 선택하여 삭제하고 다음 코드로 바꿉니다.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
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'),
),
],
),
],
),
);
}
}
// ...
저장하면 UI가 시각적으로는 완성되어 있지만 작동하지는 않습니다. 탐색 레일의 ♥︎(하트)를 클릭해도 실행되는 작업은 없습니다.
변경사항을 검토합니다.
- 먼저
MyHomePage
의 전체 콘텐츠가 새 위젯GeneratorPage
로 추출되었습니다. 이전MyHomePage
위젯에서 유일하게 추출되지 않은 부분은Scaffold
입니다. - 새
MyHomePage
에는 하위 요소 두 개가 있는Row
가 포함되어 있습니다. 첫 번째 위젯은SafeArea
이고 두 번째 위젯은Expanded
입니다. SafeArea
는 하위 요소가 하드웨어 노치나 상태 표시줄로 가려지지 않도록 합니다. 이 앱에서 이 위젯은NavigationRail
를 래핑하여 탐색 버튼이 휴대기기 상태 표시줄로 가려지지 않도록 합니다.- NavigationRail의
extended: false
줄을true
로 변경할 수 있습니다. 이렇게 하면 라벨이 아이콘 옆에 표시됩니다. 이후 단계에서는 앱에 충분한 가로 공간이 있을 때 이를 자동으로 실행하는 방법을 알아봅니다. - 탐색 레일에는 두 가지 대상(Home과 Favorites)이 있으며 각각 아이콘과 라벨이 있습니다. 탐색 레일은 현재
selectedIndex
도 정의합니다. 선택된 색인 0은 첫 번째 대상을 선택하고 선택된 색인 1은 두 번째 대상을 선택하며 이런 방식으로 계속 이어집니다. 지금은 0으로 하드 코딩되어 있습니다. - 탐색 레일은 또한 사용자가
onDestinationSelected
로 대상 중 하나를 선택할 때 발생하는 작업을 정의합니다. 지금은 앱이print()
를 사용하여 요청된 색인을 출력할 뿐입니다. Row
의 두 번째 하위 요소는Expanded
위젯입니다. Expanded 위젯은 행과 열에서 대단히 유용합니다. 이 위젯을 사용하면 일부 하위 요소는 필요한 만큼만 공간을 차지하고(여기서는NavigationRail
) 다른 위젯은 남은 공간을 최대한 차지해야 하는(여기서는Expanded
) 레이아웃을 표현할 수 있습니다.Expanded
위젯은 '탐욕스럽다'고 생각하면 됩니다. 이 위젯의 역할을 좀 더 파악하려면NavigationRail
위젯을 또 다른Expanded
위젯으로 래핑해 보세요. 결과 레이아웃이 다음과 같이 표시됩니다.
- 탐색 레일에는 실제로 왼쪽의 작은 공간만이 필요하더라도 두
Expanded
위젯은 두 위젯 사이에 사용 가능한 모든 가로 공간을 분할합니다. Expanded
위젯 내에는 색상이 지정된Container
가 있고 컨테이너 안에는GeneratorPage
가 있습니다.
스테이트리스(Stateless) 위젯과 스테이트풀(Stateful) 위젯
지금까지 MyAppState
는 모든 상태 요구사항을 처리했습니다. 지금까지 작성한 모든 위젯이 스테이트리스(Stateless)인 이유입니다. 변경 가능한 자체 상태를 포함하지 않습니다. 어떤 위젯도 스스로 변경할 수 없으며 MyAppState
를 거쳐야 합니다.
이러한 점은 곧 변경됩니다.
탐색 레일의 selectedIndex
값을 보관할 방법이 필요합니다. 또한 onDestinationSelected
콜백 내에서 이 값을 변경하려고 합니다.
selectedIndex
를 MyAppState
의 또 다른 속성으로 추가할 수 있고 효과도 있겠지만 앱 상태는 모든 위젯에 해당 값이 저장된 경우 이유 없이 빠르게 커질 수 있습니다.
일부 상태는 단일 위젯에만 관련되어 있으므로 해당 위젯과 같이 있어야 합니다.
State
가 있는 위젯 유형인 StatefulWidget
을 입력합니다. 먼저 MyHomePage
을 스테이트풀(Stateful) 위젯으로 변환합니다.
MyHomePage
의 첫 번째 줄(class MyHomePage...
로 시작하는 줄)에 커서를 두고 Ctrl+.
또는 Cmd+.
를 사용하여 Refactor 메뉴를 불러옵니다. Convert to StatefulWidget을 선택합니다.
IDE에서 새 클래스 _MyHomePageState
를 만듭니다. 이 클래스는 State
를 확장하므로 자체 값을 관리할 수 있습니다. 자체적으로 변경할 수 있습니다. 이전 스테이트리스(Stateless) 위젯의 build
메서드는 위젯에 유지되지 않고 _MyHomePageState
로 이동했습니다. 글자 그대로 이동했습니다. build
메서드에서 변경된 내용은 없습니다. 위치만 변경되었을 뿐입니다.
setState
새 스테이트풀(Stateful) 위젯은 하나의 변수 selectedIndex
만 추적하면 됩니다. 다음과 같이 _MyHomePageState
의 세 가지를 변경하세요.
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
변경사항을 검토합니다.
- 새 변수
selectedIndex
를 도입하고0
으로 초기화했습니다. - 지금까지 있던 하드 코딩
0
대신NavigationRail
정의에서 이 새 변수를 사용했습니다. onDestinationSelected
콜백이 호출되면 새 값을 콘솔로 인쇄하는 대신setState()
호출 내selectedIndex
에 할당합니다. 이 호출은 이전에 사용한notifyListeners()
메서드와 유사합니다. UI가 업데이트되는지 확인합니다.
탐색 레일이 이제 사용자 상호작용에 반응하지만 오른쪽의 확장된 영역은 동일하게 유지됩니다. 코드가 표시할 화면을 결정하는 데 selectedIndex
를 사용하지 않기 때문입니다.
selectedIndex 사용
_MyHomePageState
의 build
메서드 상단, return Scaffold
바로 앞에 다음 코드를 배치합니다.
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
이 코드를 검토합니다.
- 이 코드는
Widget
유형의 새 변수page
를 선언합니다. - 그런 다음,
selectedIndex
의 현재 값에 따라 switch 문이 화면을page
에 할당합니다. - 아직
FavoritesPage
가 없으므로 배치하는 곳마다 교차 사각형을 그려 UI의 해당 부분이 미완성임을 표시하는 편리한 위젯인Placeholder
를 사용합니다.
- fail-fast 원칙을 적용하면 switch 문이
selectedIndex
가 0도 1도 아닌 경우 오류가 발생하도록 합니다. 이렇게 하면 향후 있을 버그를 방지할 수 있습니다. 탐색 레일에 새 대상을 추가하고 이 코드를 업데이트하지 않은 경우 프로그램이 개발 중에 다운됩니다(왜 작동하지 않는지 추측하도록 하거나 버그가 있는 코드를 프로덕션 환경으로 게시하도록 하는 대신).
이제 page
에 오른쪽에 표시하려는 위젯이 포함되어 있으므로 다른 필요한 변경사항을 추측할 수 있습니다.
다음은 나머지 하나의 변경사항을 적용한 후의 _MyHomePageState
입니다.
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
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, // ← Here.
),
),
],
),
);
}
}
// ...
이제 앱이 GeneratorPage
와 곧 Favorites 페이지가 될 자리표시자 간에 전환됩니다.
반응성
이제 탐색 레일을 반응형으로 만듭니다. 즉, 공간이 충분하면 자동으로 라벨을 표시하도록 만듭니다(extended: true
사용).
Flutter는 앱이 자동으로 반응하도록 할 수 있는 위젯을 여러 개 제공합니다. 예를 들어 Wrap
은 세로 또는 가로 공간이 충분하지 않으면 하위 요소를 자동으로 다음 '줄'에 래핑하는('run'이라고 함) Row
또는 Column
과 유사한 위젯입니다. FittedBox
도 있습니다. 이 위젯은 사양에 따라 하위 요소를 사용 가능한 공간에 자동으로 맞춥니다.
그러나 NavigationRail
은 공간이 충분히 있을 때 자동으로 라벨을 표시하지 않습니다. 모든 컨텍스트에서 충분한 공간이 무엇인지 알 수 없기 때문입니다. 이 결정은 개발자에게 달려 있습니다.
MyHomePage
의 너비가 600픽셀 이상일 때만 라벨을 표시한다고 가정해 보겠습니다.
여기서 사용할 위젯은 LayoutBuilder
입니다. 이 위젯을 사용하면 사용할 수 있는 공간의 양에 따라 위젯 트리를 변경할 수 있습니다.
이번에도 VS Code에서 Flutter의 Refactor 메뉴를 사용하여 필요한 변경을 실행합니다. 이번에는 좀 더 복잡합니다.
_MyHomePageState
의build
메서드에서Scaffold
에 커서를 둡니다.Ctrl+.
(Windows/Linux) 또는Cmd+.
(Mac)를 사용하여 Refactor 메뉴를 불러옵니다.- Wrap with Builder를 선택하고 Enter를 누릅니다.
- 새로 추가한
Builder
를LayoutBuilder
로 이름을 수정합니다. - 콜백 매개변수 목록을
(context)
에서(context, constraints)
로 수정합니다.
LayoutBuilder
의 builder
콜백은 제약 조건이 변경될 때마다 호출됩니다. 예를 들어 다음과 같은 경우 발생합니다.
- 사용자가 앱의 창 크기를 조절합니다.
- 사용자가 휴대전화를 세로 모드에서 가로 모드로 또는 그 반대로 회전합니다.
MyHomePage
옆에 있는 일부 위젯의 크기가 커져MyHomePage
의 제약 조건이 작아집니다.- 기타 등등
이제 코드에서 현재 constraints
를 쿼리하여 라벨을 표시할지 결정할 수 있습니다. _MyHomePageState
의 build
메서드에서 다음과 같이 한 줄을 변경합니다.
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
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,
),
),
],
),
);
});
}
}
// ...
이제 앱이 화면 크기, 방향, 플랫폼과 같은 환경에 반응합니다. 즉, 앱이 반응형이 되었습니다.
이제 남은 작업은 Placeholder
를 실제 Favorites 화면으로 대체하는 것입니다. 다음 섹션에서 설명합니다.
8. 새 페이지 추가
Favorites 페이지 대신 사용한 Placeholder
위젯을 기억하나요?
이제 이 문제를 해결해 보겠습니다.
도전해 보고 싶다면 혼자 힘으로 이 단계를 해 보세요. 새 스테이트리스(Stateless) 위젯 FavoritesPage
에 favorites
목록을 표시하고 Placeholder
대신 해당 위젯을 표시하면 됩니다.
다음은 몇 가지 도움말입니다.
- 스크롤되는
Column
을 원한다면ListView
위젯을 사용합니다. context.watch<MyAppState>()
를 사용하여 위젯에서MyAppState
인스턴스에 액세스합니다.- 새 위젯을 시도해 보려는 경우
ListTile
에는title
(일반적으로 텍스트용),leading
(아이콘 또는 아바타용),onTap
(상호작용용)과 같은 속성이 있습니다. 하지만 이미 아는 위젯을 사용하여 비슷한 효과를 달성할 수 있습니다. - Dart를 사용하면 컬렉션 리터럴 내에서
for
루프를 사용할 수 있습니다. 예를 들어messages
에 문자열 목록이 포함되어 있으면 다음과 같은 코드를 사용할 수 있습니다.
반면 함수 프로그래밍에 더 익숙한 경우 Dart를 사용해 messages.map((m) => Text(m)).toList()
와 같은 코드를 작성할 수도 있습니다. 물론 언제든지 위젯 목록을 만들고 build
메서드 내에서 필수적으로 추가할 수 있습니다.
직접 Favorites 페이지를 추가할 때의 장점은 스스로 결정을 내리면서 더 많이 배우게 된다는 것입니다. 단점은 아직 직접 해결할 수 없는 문제가 발생할 수 있다는 것입니다. 실패해도 괜찮으며 실패는 배움에서 가장 중요한 요소 중 하나라는 점을 기억하세요. 첫 시간부터 Flutter 개발에 성공할 거로 기대하는 사람은 없으며 개발자도 이렇게 생각해야 합니다.
다음 내용은 즐겨찾기 페이지를 구현하는 한 가지 방법입니다. 구현 방식을 통해 코드 작성에 관한 영감을 받기를 바랍니다. 즉, UI를 개선하고 나만의 UI로 만들어 보세요.
다음은 새 FavoritesPage
클래스입니다.
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@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),
),
],
);
}
}
위젯의 역할은 다음과 같습니다.
- 앱의 현재 상태를 가져옵니다.
- 즐겨찾기 목록이 비어 있으면 No favorites yet이라는 메시지를 중앙에 표시합니다*.*
- 목록이 비어 있지 않으면 스크롤 가능한 목록을 표시합니다.
- 목록은 요약으로 시작됩니다(예: You have 5 favorites*.*).
- 그런 다음 코드가 모든 즐겨찾기를 반복하고 각 즐겨찾기의
ListTile
위젯을 구성합니다.
이제 남은 작업은 Placeholder
위젯을 FavoritesPage
로 바꾸는 것입니다. 완성되었습니다.
이 앱의 최종 코드는 GitHub의 Codelab 저장소에서 확인할 수 있습니다.
9. 다음 단계
축하합니다.
성공했습니다. Column
하나와 Text
위젯 두 개가 포함된 작동하지 않는 스캐폴드로 재미있는 반응형의 작은 앱을 만들었습니다.
학습한 내용
- Flutter 작동 방식의 기본사항
- Flutter에서 레이아웃 만들기
- 버튼 누르기 등 사용자 상호작용을 앱 동작에 연결
- Flutter 코드 체계적으로 유지
- 앱을 반응형으로 만들기
- 앱의 일관된 디자인과 분위기 달성
다음 단계
- 이 실습에서 작성한 앱으로 더 많이 실험해 봅니다.
- 동일한 앱의 이 고급 버전 코드를 살펴보고 애니메이션 목록, 그래디언트, 크로스 페이드 등을 추가할 수 있는 방법을 확인합니다.
- flutter.dev/learn으로 이동하여 학습 여정을 계속합니다.