GestureDetector 위젯
- GestureDetector는 사용자가 화면에서 수행하는 다양한 터치 이벤트를 감지하고 처리하는 기본적인 Flutter 위젯입니다.
- GestureDetector 자체는 화면에 표시되지 않으며, child에 지정된 위젯에 사용자 이벤트가 발생할 때 이벤트를 처리할 수 있습니다.
- 이 위젯은 다양한 사용자 이벤트에 대한 콜백 함수들로 구성되어 있어, 각 이벤트에 맞는 동작을 정의할 수 있습니다.
주요 GestureDetector 콜백 함수
- onTap: 사용자가 화면을 가볍게 탭할 때 호출됩니다. (예: 버튼 클릭 효과)
- onDoubleTap: 사용자가 화면을 빠르게 두 번 탭할 때 호출됩니다. (예: 이미지 확대)
- onLongPress: 사용자가 화면을 오래 누르고 있을 때 호출됩니다. (예: 아이템 삭제 옵션 표시)
- onTapDown: 사용자가 화면을 터치하기 시작할 때 호출됩니다. (예: 버튼을 누르는 즉시 효과 적용)
- onTapUp: 사용자가 터치한 손가락을 화면에서 떼었을 때 호출됩니다. (예: 클릭 완료)
- onVerticalDragStart: 사용자가 화면을 위아래로 드래그하기 시작할 때 호출됩니다. (예: 목록 스크롤 시작 감지)
- onHorizontalDragStart: 사용자가 화면을 좌우로 드래그하기 시작할 때 호출됩니다. (예: 슬라이드 메뉴 호출)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('드래그 기능 만들어 보기'),
),
body: DraggableBox(),
),
);
}
}
class DraggableBox extends StatefulWidget {
const DraggableBox({super.key});
@override
State<DraggableBox> createState() => _DraggableBoxState();
}
class _DraggableBoxState extends State<DraggableBox> {
double _xOffset = 2.0; // x 축 이동 값
double _yOffset = 5.0; // y 축 이동 값
@override
Widget build(BuildContext context) {
return GestureDetector(
// 드래그가 업데이트 될 때 호출되는 콜백 함수
onPanUpdate: (details) {
setState(() {
_xOffset += details.delta.dx; // x 축 방향으로 이동한 값
_yOffset += details.delta.dy; // y 축 방향으로 이동한 값
});
},
child: Stack(
children: [
Positioned(
left: _xOffset,
top: _yOffset,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(width: 1.0, color: Colors.redAccent),
borderRadius: BorderRadius.circular(8.0)
),
),
)
],
),
);
}
}
Flutter에서의 위젯 생명 주기
Flutter에서 위젯의 생명 주기는 중요한 개념입니다. 특히, StatelessWidget과 StatefulWidget은 동작 방식이 다르기 때문에 각각의 생명 주기를 이해하는 것이 중요합니다.
State 생명 주기
- StatelessWidget과 StatefulWidget은 빌드될 때마다 새로 생성됩니다.
- StatelessWidget은 build 메서드가 호출되면서 한 번만 생성되고 끝납니다.
- StatefulWidget은 내부적으로 State 객체를 생성하며, 생성된 State 객체는 메모리에 유지되면서 생명 주기를 가집니다.
- 한 번 생성된 State는 재사용되며, 필요할 때만 build 메서드가 다시 호출되어 업데이트됩니다.
StatefulWidget의 생명 주기
- StatefulWidget의 생명 주기는 다음과 같은 주요 메서드로 동작함
- createState() : StatefulWidget에서는 createState() 메서드를 통해 State 객체를 생성해야 하며, 전체 생명 주기 중, 한 번만 호출됨
class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); }
- initState() : State 객체가 생성된 후 호출되며, 위젯 상태를 초기화하는 데 보통 사용됨. 전체 생명 주기 중, 한 번만 호출됨
- build() : initState() 호출 후, 호출되며, 개발자가 setState()를 호출하면, build() 메서드가 재호출됨.
- 호출 시마다, 변경된 상태를 기반으로 변경된 UI를 표현
- dispose() : 위젯 트리에서 제거될 때 호출되며, State 객체가 영구적으로 제거되고, 사용 자원이 해제됨
import 'package:flutter/material.dart';
void main() {
runApp(MyWidget());
}
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
print('initState called');
}
@override
Widget build(BuildContext context) {
print('build called');
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('My Widget'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Button tapped $_counter times.'),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
child: Text('Tap me'),
onPressed: () {
setState(() {
_counter++;
});
},
),
),
],
),
),
),
);
}
@override
void dispose() {
print('dispose called'); // 위젯 제거 시, dispose() 호출됨. 이때 메모리상에 할당된 모든 자원 해제해야 함.
super.dispose();
}
}
- 애니매이션 코드 사용해보기
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(MyWidget());
}
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
int _counter = 0; // 1. 버튼이 눌린 횟수를 저장하는 변수입니다.
Color _backgroundColor = Colors.white; // 2. 배경색을 저장하는 변수입니다.
late AnimationController _controller; // 3. 애니메이션의 진행을 제어하는 컨트롤러입니다.
late Animation<double> _animation; // 4. 애니메이션의 스케일 값을 저장하는 변수입니다.
@override
void initState() {
super.initState();
print('initState called'); // initState는 위젯이 처음 생성될 때 한 번 호출됩니다.
// AnimationController 초기화
_controller = AnimationController(
duration: const Duration(milliseconds: 300), // 5. 애니메이션의 지속 시간을 설정합니다.
vsync: this, // 6. vsync는 화면 새로고침 주기에 동기화하여 애니메이션 성능을 최적화합니다.
);
// Tween을 사용하여 애니메이션 범위 정의 (0.8 ~ 1.0)
_animation = Tween<double>(begin: 0.8, end: 1.0).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut, // 7. 애니메이션이 부드럽게 시작하고 끝나도록 설정합니다.
))
..addListener(() {
setState(() {}); // 8. 애니메이션이 진행될 때마다 화면을 업데이트합니다.
});
}
@override
void dispose() {
_controller.dispose(); // 9. 메모리 누수를 방지하기 위해 애니메이션 컨트롤러를 해제합니다.
print('dispose called'); // dispose는 위젯이 제거될 때 한 번 호출됩니다.
super.dispose();
}
// 버튼을 눌렀을 때 호출되는 함수
void _incrementCounter() {
setState(() {
_counter++; // 10. 버튼이 눌릴 때마다 카운터를 증가시킵니다.
_backgroundColor = _getRandomColor(); // 11. 배경색을 랜덤으로 변경합니다.
});
// 12. 애니메이션을 앞으로 진행한 후, 완료되면 원래 상태로 되돌립니다.
_controller.forward().then((_) {
_controller.reverse();
});
}
// 랜덤 색상을 생성하는 함수
Color _getRandomColor() {
final random = Random();
return Color.fromARGB(
255,
random.nextInt(256), // R (0~255) 랜덤 값
random.nextInt(256), // G (0~255) 랜덤 값
random.nextInt(256), // B (0~255) 랜덤 값
);
}
@override
Widget build(BuildContext context) {
print('build called'); // build는 상태가 변경될 때마다 호출됩니다.
return MaterialApp(
theme: ThemeData(
useMaterial3: true, // 13. Material 3 스타일을 적용합니다.
colorSchemeSeed: Colors.blue, // 14. Material 3 컬러 테마의 기본 색상을 지정합니다.
),
home: Scaffold(
appBar: AppBar(
title: Text('Animated Counter'),
),
backgroundColor: _backgroundColor, // 15. 배경색을 설정합니다.
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'버튼을 누른 횟수 : $_counter 번', // 16. 버튼이 눌린 횟수를 텍스트로 표시합니다.
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
// 17. Transform.scale 위젯으로 버튼의 크기를 애니메이션 효과로 조절합니다.
Transform.scale(
scale: _animation.value, // 18. 애니메이션 스케일 값을 적용합니다.
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
backgroundColor: Colors.blueAccent, // 19. 버튼 배경색 설정
),
child: Text('눌러 보기', style: TextStyle(fontSize: 18)),
onPressed: _incrementCounter, // 20. 버튼을 누르면 _incrementCounter 함수 호출
),
),
],
),
),
),
);
}
}
코드 설명
- AnimationController 초기화: _controller는 애니메이션의 타이밍을 제어하는 객체로, duration을 통해 애니메이션 속도를 조절하고, vsync는 SingleTickerProviderStateMixin을 통해 화면 새로고침 주기에 동기화하여 성능을 최적화합니다.
- Tween 객체 설정: Tween<double>(begin: 0.8, end: 1.0)은 애니메이션이 시작될 때는 0.8배의 크기에서 시작해, 1.0배 크기로 변하는 애니메이션 효과를 제공합니다.
- Transform.scale 위젯: scale 속성에 애니메이션 값을 적용해 버튼의 크기가 점점 커졌다가 다시 원래대로 돌아오도록 설정합니다.
Flutter에서 애니메이션을 효율적으로 관리하기 위해 vsync와 SingleTickerProviderStateMixin을 사용합니다
- vsync는 "Vertical Synchronization"의 약자로, 애니메이션이 화면의 새로고침 주기와 동기화되도록 도와줍니다.
- 목적: 애니메이션이 화면에 보이지 않거나 필요하지 않은 경우, 불필요하게 자원을 사용하지 않도록 애니메이션을 자동으로 멈춰줍니다.
- 효과: 화면 주기와 맞춰 애니메이션이 실행되므로 CPU와 GPU의 자원 사용을 줄여 효율적으로 동작합니다.
2. SingleTickerProviderStateMixin의 역할
- Ticker는 Flutter에서 애니메이션을 위한 핵심 요소로, 화면을 여러 번 새로 그려 애니메이션을 부드럽게 보여줍니다.
- SingleTickerProviderStateMixin은 Ticker를 한 번만 제공하여, 애니메이션이 필요할 때만 활성화하고 그렇지 않을 때는 비활성화합니다.
- 장점: 애니메이션이 없는 상황에서 Ticker가 불필요하게 작동하는 것을 방지해, 메모리와 자원을 절약합니다.
왜 SingleTickerProviderStateMixin을 써야 할까요?
애니메이션은 Ticker의 도움으로 화면을 반복해서 새로 그리며 동작합니다. 하지만 필요하지 않은 상황에서도 Ticker가 작동하면 자원을 낭비하게 되죠. SingleTickerProviderStateMixin을 사용하면 필요할 때만 Ticker가 작동하도록 관리할 수 있어, 애니메이션을 효율적으로 실행할 수 있습니다.
Flutter의 위젯과 Element 트리 그리고 Key의 역할과 사용 방법
- Flutter는 각 위젯마다 Element 객체를 생성해 트리 구조를 만듭니다.
- Element는 위젯의 타입, 위치 정보를 저장하고 자식 Element와 연결되어 전체 트리를 구성합니다
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyHomePage()));
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final listTile = <Tile>[
Tile(color: Colors.blue, name: '파란색 타일'),
Tile(color: Colors.red, name: '빨간색 타일'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('키가 없는 위젯을 만들어 보기'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: listTile,
),
),
floatingActionButton: FloatingActionButton(
onPressed: swapTiles,
child: Icon(Icons.swap_horiz),
),
);
} // end of build()
// 첫번째 타일과 두번째 타일을 교환 하는 기능
void swapTiles() {
// ['파란색타일', '빨간색타일']
setState(() {
// 첫 번째 타일을 제거하고 그 타일을 반환하는 기능
// [ '빨간색타일']
// var removedTile = listTile.removeAt(0);
// [ '빨간색타일', '파란색타일']
// 제거된 타잉르 두 번째 위치로 삽입하자.
// listTile.insert(1, removedTile);
// 축약해서 코드 작성
listTile.insert(1, listTile.removeAt(0));
});
}
} // end of state class
class Tile extends StatefulWidget {
final Color color; // 타일에 색상
final String name; // 타일에 이름
const Tile({required this.color, required this.name});
@override
State<Tile> createState() => _TileState();
}
// 커스텀 Tile 클래스의 상태 관리 클래스 입니다.
class _TileState extends State<Tile> {
// State 멤버 변수 widget 이런 멤버 변수를 제공해서 상위 클래스이 접근 할 수
// 있도록 만들어 주고 있다.
Color? currentColor; // 현재 타일에 색상을 지정
@override
void initState() {
super.initState();
// 부모 클래스 변수에 접근에서 값을 가져 옴
currentColor = widget.color;
}
@override
Widget build(BuildContext context) {
return Container(
color: currentColor,
width: 100,
height: 100,
alignment: Alignment.center,
child: Text(
widget.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
}
State 클래스는 StatefulWidget과 연결된 상태 관리 클래스로, Flutter에서 위젯의 상태를 저장하고 변경하는 역할을 담당합니다. State 클래스는 부모 Widget에 연결된 속성이나 값에 직접 접근할 수 없기 때문에, widget 객체를 통해 부모 Widget의 속성 값을 사용할 수 있습니다.
Flutter는 위젯 트리에서 동일한 타입의 위젯을 재사용합니다. 위젯의 순서가 바뀌거나 새로 추가되는 경우, 이전 상태를 그대로 이어가게 되어 UI 버그가 발생할 수 있습니다. 예를 들어, 리스트에서 순서를 바꾸면 Flutter는 기존의 상태를 재사용하여 버그가 발생할 수 있습니다.
💡Key를 사용하지 않아 위젯을 교환할 때 상태가 제대로 업데이트되지 않음
Flutter에서 Key의 역할과 필요성
Key는 Flutter 애플리케이션에서 위젯의 고유성을 보장하고, 상태를 관리하는 데 중요한 역할을 합니다. 특히, 위젯 트리에서 요소의 순서가 변경되거나 목록이 동적으로 업데이트될 때 Key는 의도하지 않은 UI 동작을 방지하는 핵심 요소입니다.
Key란 무엇인가?
- Flutter는 모든 위젯마다 Element라는 객체를 내부적으로 생성해 트리를 만듭니다.
- Flutter에서 Key는 각 위젯을 고유하게 식별하기 위한 속성입니다.
- 복잡한 위젯 트리 구조에서 위치가 바뀌거나, 재구성될 때 위젯을 정확히 식별하는 데 중요한 역할을 합니다.
Key의 작동 방식
Key 를 활용한 UI 버그를 방지하는 방법 확인
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyHomePage()));
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final listTile = <Tile>[
Tile(key: ValueKey('파란색'), color: Colors.blue, name: '파란색 타일'),
Tile(key: ValueKey('빨간색'), color: Colors.red, name: '빨간색 타일'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('키가 없는 위젯을 만들어 보기'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: listTile,
),
),
floatingActionButton: FloatingActionButton(
onPressed: swapTiles,
child: Icon(Icons.swap_horiz),
),
);
} // end of build()
// 첫번째 타일과 두번째 타일을 교환 하는 기능
void swapTiles() {
// ['파란색타일', '빨간색타일']
setState(() {
// 첫 번째 타일을 제거하고 그 타일을 반환하는 기능
// [ '빨간색타일']
// var removedTile = listTile.removeAt(0);
// [ '빨간색타일', '파란색타일']
// 제거된 타잉르 두 번째 위치로 삽입하자.
// listTile.insert(1, removedTile);
// 축약해서 코드 작성
listTile.insert(1, listTile.removeAt(0));
});
}
} // end of state class
class Tile extends StatefulWidget {
final Color color; // 타일에 색상
final String name; // 타일에 이름
//Key 속성을 추가해서 부모클래스 (Sate)고유 식별자를 보장한다.
const Tile({required Key key,
required this.color, required this.name}) : super(key: key);
@override
State<Tile> createState() => _TileState();
}
// 커스텀 Tile 클래스의 상태 관리 클래스 입니다.
class _TileState extends State<Tile> {
// State 멤버 변수 widget 이런 멤버 변수를 제공해서 상위 클래스이 접근 할 수
// 있도록 만들어 주고 있다.
Color? currentColor; // 현재 타일에 색상을 지정
@override
void initState() {
super.initState();
// 부모 클래스 변수에 접근에서 값을 가져 옴
currentColor = widget.color;
}
@override
Widget build(BuildContext context) {
return Container(
color: currentColor,
width: 100,
height: 100,
alignment: Alignment.center,
child: Text(
widget.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
}
핵심 정리
1. **Key의 기본 이해**:
- Key는 위젯 트리에서 **각 위젯의 고유성을 보장**하여 Flutter가 상태를 일관되게 관리하도록 도와줍니다. Key는 특히 위젯의 순서가 바뀌거나 새롭게 추가될 때 중요한 역할을 합니다.
2. **Key를 사용하지 않을 때 발생하는 문제**:
- Flutter는 동일한 타입의 위젯을 재사용하기 때문에, 순서 변경이나 상태 관리에 버그가 발생할 수 있습니다. Key가 없을 경우 Flutter는 어떤 위젯이 기존의 것인지 새로 생성된 것인지 구별하지 못할 수 있습니다.
3. **UI 버그 해결**:
- 리스트나 Row, Column과 같은 여러 위젯이 반복되는 레이아웃에서 Key를 사용하면 Flutter는 **각 위젯을 고유하게 인식**할 수 있으며, 이를 통해 UI가 의도치 않게 변경되는 것을 방지할 수 있습니다.
4. **Key의 종류와 사용 방법**:
- ValueKey와 같은 고유한 값 기반의 Key, UniqueKey와 같이 무작위로 생성되는 Key등이 있습니다
- 각각의 Key는 특정 시나리오에서 유용하게 사용되며, 상태를 안전하게 유지하면서 UI 버그를 방지하는 데 도움이 됩니다.
'Flutter > Flutter UI 프레임워크' 카테고리의 다른 글
riverpod 과 MVVM 활용 (0) | 2024.12.11 |
---|---|
MVVM 패턴과 상태 관리 (0) | 2024.12.11 |
# Dio 통신 연습 (0) | 2024.12.11 |
플러터 기본기 다지기 - 3 (0) | 2024.12.11 |
플러터 기본기 다지기 - 2 (2) | 2024.11.21 |