✨ Today I Learned 3회차 - Flutter 위젯 심화 🎯
🎯 Flutter 위젯 심화
🔄 3-1. StatefulWidget
정의: UI 상태 변경이 가능한 위젯 ✨
다만 실제로 UI 업데이트를 진행할 때 UI를 새로 그려달라는 요청을 해야한다. 곧 build 메서드를 다시 한번 호출해 달라는 요청이다.
➡️ 이것을 setState()
로 가능하다.
➡️ setState
를 호출하면 build 함수가 다시 한번 호출되는 형태이다.
💡 이것을 통해 알 수 있는 사실은 Widget의 build 함수가 결국 UI를 그리는 작업을 담당하고 있다.
⚡ 실제 StatefulWidget을 구현할 때는 StatelessWidget을 먼저 생성하고 quick fix 기능으로 변경을 해보자.
🔄 StatefulWidget LifeCycle
메서드 이름 | 호출 시점 및 조건 | 주요 역할 |
---|---|---|
createState() |
StatefulWidget 이 처음 생성될 때 |
State 객체를 생성하여 연결 🔗 |
initState() ⭐ |
State 가 처음 초기화될 때 1회 호출 |
초기 상태 설정, 리스너 등록 ⚙️ |
didChangeDependencies() |
initState() 이후 또는 InheritedWidget 이 변경되었을 때 |
의존성 관련 값 접근 가능 🔄 |
build() ⭐ |
위젯이 처음 그려질 때, 또는 setState() /didUpdateWidget() 호출 후 |
UI 구성 🎨 |
didUpdateWidget() |
부모가 위젯을 새 인스턴스로 다시 그릴 때 (State는 유지) | 새 파라미터 반영. 이후 자동으로 build() 🔄 |
setState() |
상태 변경 발생 시 호출 | 상태 변경 → build() 트리거 ⚡ |
deactivate() |
위젯이 트리에서 제거되기 직전 | 위젯이 제거될 준비 단계 ⏳ |
dispose() ⭐ |
위젯이 완전히 트리에서 제거될 때 | 리소스 해제, 컨트롤러/리스너 정리 🗑️ |
⌨️ 3-2. TextField
사용자의 글자 입력을 받을 수 있는 위젯 📝
📝 주요 속성
textInputAction
⌨️- 키보드의 액션 버튼(오른쪽 아래 버튼)의 형태와 동작을 결정 🎯
| 타입명 | 키보드 버튼에 표시되는 텍스트 | 용도/설명 |
| — | — | — |
|
TextInputAction.done
| Done ✅ | 입력 완료 시 사용 (폼 제출, 입력 종료) | |TextInputAction.next
| Next ➡️ | 다음 입력 필드로 이동 | |TextInputAction.previous
| Previous ⬅️ | 이전 입력 필드로 이동 (드물게 사용됨) | |TextInputAction.search
| Search 🔍 | 검색 창 등에서 검색 트리거 | |TextInputAction.send
| Send 📤 | 채팅/메시지 등에서 전송 기능 | |TextInputAction.go
| Go 🚀 | URL 이동, 진행 버튼 등에 사용 | |TextInputAction.continueAction
| Continue ⏩ | 다음 단계로 계속 진행할 때 (iOS에서 주로 사용) | |TextInputAction.join
| Join 🤝 | 가입이나 참여 관련 입력창 | |TextInputAction.route
| Route 🗺️ | 경로 탐색, 내비게이션 목적의 입력 | |TextInputAction.emergencyCall
| Emergency 🚨 | 긴급 전화 관련 UI | |TextInputAction.newline
| Enter / 줄 바꿈 📝 | 멀티라인 입력 시 줄 바꿈 기능 (채팅/메모장 등에서 자주 사용) | |TextInputAction.unspecified
| 플랫폼 기본값 사용 ⚙️ | 특별히 지정하지 않음 (기본 동작 사용) |
- 키보드의 액션 버튼(오른쪽 아래 버튼)의 형태와 동작을 결정 🎯
| 타입명 | 키보드 버튼에 표시되는 텍스트 | 용도/설명 |
| — | — | — |
|
expands
📏- TextField가 부모 위젯의 남은 세로 공간을 전부 차지
maxLines: null
과 함께 반드시 사용해야 동작함 ⚠️
maxLines
📊- 줄 수 제한
- null 일 땐 줄 수 제한 없음 ♾️
onChanged
📝- 사용자가 입력을 변경할 때마다 호출
- 실시간 입력 감지, 자동 저장 등에 활용 가능 ⚡
onSubmitted
✅- 사용자가 Enter 키를 눌러 입력을 제출했을 때 호출
- 작성 완료 처리, 입력값 저장 등에 사용 💾
🎮 TextEditingController
TextField의 입력 값을 직접 제어하거나 읽을 수 있도록 하는 객체 🎯
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Test")),
body: Column(
children: [
TextField(controller: controller),
ElevatedButton(
onPressed: () {
controller.clear();
},
child: Text("초기화"),
),
ElevatedButton(
onPressed: () {
print(controller.text);
},
child: Text("현재값"),
),
],
),
);
}
}
TextField를 사용하고 싶을 때 사용 📱
- 입력 초기값 설정, clear 등 다양한 작업 가능 ⚙️
- 더이상 사용하지 않을 때 dispose해줘야 한다(메모리 누수 방지) ⚠️
🎨 TextField 꾸미기
✨ Style 관련 속성
decoration
: 입력창 꾸미기 🎨 | 속성명 | 설명 | | — | — | |labelText
| 입력창 위에 항상 표시되는 라벨 텍스트 🏷️ | |hintText
| 입력 전, 입력창 안에 흐릿하게 보이는 힌트 💭 | |prefixIcon
/suffixIcon
| 입력창 안쪽에 표시할 아이콘 🔍 | |prefix
/suffix
| 아이콘 외에 텍스트/위젯도 가능 📝 | |filled
| 배경을 채울지 여부 (true 설정 시 fillColor 필요) ✨ | |fillColor
| 배경색 설정 🎨 | |border
| 기본 상태 테두리 📦 | |enabledBorder
| 포커스되지 않은 상태의 테두리 ⚪ | |focusedBorder
| 포커스된 상태의 테두리 🔵 | |contentPadding
| 내부 여백 (입력값과 테두리 사이 간격) 📏 |style
: 입력 텍스트 스타일 ✍️-
cursorColor
,cursorWidth
: 커서 색상/굵기 textAlign
: 정렬 위치 📍obscureText
: 비밀번호 처리 🔒
📋 사용 예제
TextField(
decoration: InputDecoration(
labelText: '이메일',
hintText: 'example@domain.com',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey[100],
),
style: TextStyle(fontSize: 16, color: Colors.black87),
cursorColor: Colors.blueAccent,
)
🧪 실습 코드
@override
Widget build(BuildContext context) {
print("build 메서드 호출됨");
return GestureDetector(
onTap: () {
FocusScope.of(
context,
).unfocus(); // TextField를 다룰 때는 Scaffold를 GestureDetector로 감싸서 포커스를 해제해주는 동작을 반드시 수행
},
child: Scaffold(
appBar: AppBar(
title: Text("카운터"),
backgroundColor: Colors.purple.shade100,
centerTitle: true,
),
body: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 200,
child: TextField(
controller: controller,
decoration: InputDecoration(
labelText: "라벨",
hintText: "힌트",
prefixIcon: Icon(Icons.search),
suffixIcon: Icon(Icons.add_call),
// fillColor: Colors.amber,
// filled: true, // 이 값을 true로 줘야 fillColor가 적용
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(width: 20),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
contentPadding: EdgeInsets.all(0),
),
textInputAction: TextInputAction.go,
maxLines: 1,
onChanged: (value) {
print(value);
},
onSubmitted: (value) => {print("onSubmit: $value")},
cursorColor: Colors.green,
cursorHeight: 10,
),
),
ElevatedButton(
onPressed: () {
controller.clear();
print(controller.text);
},
child: Text("Increment"),
),
],
),
),
),
);
}
📜 3-3. ListView
스크롤 가능한 위젯 리스트를 만들 때 사용 📱
💡 리스트뷰는 화면에 보이는 위젯만 그리기 때문에 성능상 좀더 효율이 좋다. (SingleChildScrollView는 모두 그리고 있기 때문에 ListView를 사용하는 게 더 좋다) ⚡
🔨 ListView.builder
for문처럼 인덱스를 기반으로 위젯을 하나씩 만들어줌 🔁
body: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: double.infinity,
height: 400,
color: Colors.amber,
margin: EdgeInsets.all(20),
child: Center(child: Text("1", style: TextStyle(fontSize: 50))),
);
},
),
💡 builder도 화면이 보이는 위젯만 그때 그때 그려줌 ⚡
📐 ListView.separated
List를 그릴 때 중간 중간 여백을 둘 때 사용 🔲
body: ListView.separated(
itemCount: 5,
separatorBuilder: (context, index) {
return SizedBox(height: 100);
},
itemBuilder: (context, index) {
return Container(
width: double.infinity,
height: 400,
color: Colors.amber,
margin: EdgeInsets.all(20),
child: Center(child: Text("1", style: TextStyle(fontSize: 50))),
);
},
)
📱 3-4. ButtomNavigationBar + IndexedStack
📱 ButtomNavigationBar
- 화면 하단에 고정된 탭 메뉴를 만드는데 사용 📍
- 화면 하단에 고정된 탭 메뉴를 만드는 데 사용 🎯
- 앱에서 여러 페이지(예: 홈, 설정, 프로필 등)를 전환할 수 있게 함 🔄
- 일반적으로
StatefulWidget
과 함께 사용하여 선택된 탭 인덱스를 상태로 관리함 📊 Scaffold
의bottomNavigationBar
속성에 사용 🏗️
📝 주요 속성
items
: 탭으로 표시할 아이콘과 라벨 리스트 📋currentIndex
: 현재 선택된 탭 인덱스 📍onTap
: 탭 터치 시 호출되는 함수(터치된 탭의 인덱스가 인자로 넘어옴) 👆selectedItemColor
: 선택된 탭의 아이콘과 텍스트 색상 🔵iconSize
: 아이콘 크기 설정 (기본값은 24.0) 📏selectedLabelStyle
: 선택된 탭의 텍스트 스타일 (ex. 볼드체) ✨unselectedItemColor
: 선택되지 않은 탭의 색상 (생략 가능, 기본은 회색) ⚪unselectedLabelStyle
: 선택되지 않은 텍스트 스타일도 따로 설정 가능 📝
📋 사용 예제
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(title: Text("바텀내비")),
bottomNavigationBar: BottomNavigationBar(
currentIndex: index,
onTap: (value) {
setState(() {
index = value;
});
},
selectedItemColor: Colors.red,
iconSize: 30, // 기본 사이즈 24
selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold),
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "홈"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "설정"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "마이페이지"),
],
),
);
}
📚 IndexedStack
여러 위젯을 겹쳐서(stack) 배치한 뒤, 그 중 지정한 index에 해당하는 위젯만 보여주는 위젯 🎯
📝 주요 속성
index
📍- 현재 표시할 자식 위젯의 인덱스
- 해당 인덱스에 해당하는 위젯만 화면에 보임 👁️
children
👶- 겹쳐놓을 위젯 리스트
- 모든 위젯이 메모리에는 유지됨 (build됨) 🧠
🧭 3-5. 페이지 라우팅
페이지 이동 🚀
- 새로운 페이지로 이동 ➡️
- Navigator.push()
- 뒤로 가기 ⬅️
- Navigator.pop()
🧭 Navigator란? 그리고 왜 필요할까?
Navigator
는 Flutter 앱에서 화면(Page, Route) 간 전환을 관리하는 핵심 위젯 🎯- 내부적으로 스택(Stack) 자료구조를 사용하여 📚 스택 자료구조 : 데이터를 담을 때 마지막에 넣은 데이터가 가장 먼저 나오는 구조 🔄
- 새로운 페이지는 push()로 쌓고 이전 페이지는 pop()으로 꺼내며 이동을 처리 ⚡
- PageA → PageB 이동 시 라우트 스택 상황
[PageA] // 첫 페이지
[PageB] // 첫 페이지
[PageA] // 첫 페이지
┌──────────────────────────────┐ │ MaterialApp │ │ (Navigator 🧭 내장되어 있음) │ └──────────────────────────────┘ ⬆️ ┌────────────────────┐ │ PageA(context) │ └────────────────────┘ ⬆️ ┌────────────────────┐ │ Scaffold │ └────────────────────┘ ⬆️ ┌────────────────────┐ │ ElevatedButton │ └────────────────────┘
🤔 페이지 이동하는데 context
는 왜 필요할까?
context
는 Flutter 위젯 트리 내에서 현재 위젯의 위치와 관련된 정보를 담고 있는 객체 📍- 정확한 타입은
BuildContext
이며, 이를 통해 Flutter는 위젯 트리를 상위로 타고 올라가며 필요한 위젯을 찾을 수 있음 🔍 - Navigator 위젯을 찾아서 push 의 두번째 인자에서 전달해준 위젯을 스택에 push! 🎯
📦 데이터 전달
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("APage"),
), // 뒤로 가야하는 페이지가 있을 때 자동으로 뒤로 가기 버튼이 생성됨
body: Column(
children: [
ElevatedButton(
onPressed: () async {
String? result = await Navigator.push(
// 반환 받은 문자열이 null일 수 있으니 nullable 타입을 사용하자
context,
MaterialPageRoute(builder: (context) => PageB("안녕하세요")),
);
print("페이지 이동 완료");
print(result);
},
child: Text("B페이지로 이동"),
),
],
),
);
}
}
class PageB extends StatelessWidget {
PageB(this.value);
String value;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("BPage")),
body: Column(
children: [
Text(value, style: TextStyle(fontSize: 100)),
ElevatedButton(
onPressed: () {
Navigator.pop(context, "돌아갑니다");
},
child: Text("뒤로가기"),
),
],
),
);
}
}
💡 데이터 전달 방식
- push를 할 때
MaterialPageRoute
를 사용해서 이동할 페이지를 생성할 때 생성자를 통해 데이터를 전달해 줄 수 있고 📤 - pop을 할 때는 return 타입이 Future인 점을 활용해서 push 시점에 async await을 사용해서 전달 받은 데이터를 활용할 수 있다 📥
📝 3-6. 투두앱 만들기 - UI 구현하기
UI를 구현할 때 가장 먼저 해야하는 것이 있다. 바로 레이아웃 나누기! 🎯
전체 페이지에서의 골격은 Scaffold가 나눠줌 🏗️
- Scaffold appBar 📱
- Scaffold body 📄
- Scaffold bottomSheet 📋
투두 아이템을 길게 눌렀을 때 iOS 스타일의 팝업 노출 📱
📋 TODO 앱을 만드는 과정
- UI를 보면서 레이아웃을 먼저 잡는다 👀
- 크게 3가지 영역으로 잡아 본다 📐
- appBar(상단) 📱
- body(중단) 📄
- bottomSheet(하단) 📋
- 크게 3가지 영역으로 잡아 본다 📐
- MaterialApp에서 home에 들어갈 위젯을 생성 🏠
- 홈 위젯에서 화면의 구조를 잡을 Scaffold 위젯을 생성 🏗️
- AppBar 영역 구현 📱
- Title 텍스트 생성 ✨
- BottomSheet 영역 구현 📋
- TextField 생성 ⌨️
- Container: bottom 배경 색상과 padding을 위해 📦
- TextField 📝
- controller 🎮
- decoration: InputDecoration 🎨
- hintText 💭
- border 📦
- fillColor 🎨
- filled ✨
- subfixIcon 🔍
- GestureDetector 👆
- child 👶
- Container 📦
- margin 📏
- decoration: BoxDecoration 🎨
- child 👶
- Icon 🔍
- Container 📦
- TextField 📝
- Container: bottom 배경 색상과 padding을 위해 📦
- TextField 생성 ⌨️
- Body 영역 구현 📄
- Todo 리스트 박스 레이아웃 나누기 📐
- Scoller 영역을 위해 ListView로 구현 📜
- padding: ListView 아이템들의 여백을 위해서 📏
- children 👶
- Container 📦
- width: double.infinity(가로 끝까지) ↔️
- decoration: BoxDecoration 🎨
- border 📦
- borderRadius 🔘
- padding 📏
- child 👶
- Row ↔️
- children 👶
- Container 📦
- width 📏
- height 📏
- decoration: BoxDecoration 🎨
- border: Box 색상 🎨
- shape: Box의 모양 🔘
- SizedBox 📏
- Expanded: 자동으로 다음 칸으로 넘어가기 위해서 ↔️
- Text ✍️
- Sized Box(끝 여백) 📏
- Container 📦
- children 👶
- Row ↔️
- SizedBox 📏
- Container 📦
댓글남기기