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에서 위젯의 생명 주기는 중요한 개념입니다. 특히, StatelessWidgetStatefulWidget은 동작 방식이 다르기 때문에 각각의 생명 주기를 이해하는 것이 중요합니다.

State 생명 주기

  1. StatelessWidget과 StatefulWidget은 빌드될 때마다 새로 생성됩니다.
  2. StatelessWidget은 build 메서드가 호출되면서 한 번만 생성되고 끝납니다.
  3. 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 함수 호출
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

코드 설명

  1. AnimationController 초기화: _controller는 애니메이션의 타이밍을 제어하는 객체로, duration을 통해 애니메이션 속도를 조절하고, vsync는 SingleTickerProviderStateMixin을 통해 화면 새로고침 주기에 동기화하여 성능을 최적화합니다.
  2. Tween 객체 설정: Tween<double>(begin: 0.8, end: 1.0)은 애니메이션이 시작될 때는 0.8배의 크기에서 시작해, 1.0배 크기로 변하는 애니메이션 효과를 제공합니다.
  3. 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의 작동 방식

Flutter는 Key를 통해 위젯을 고유하게 식별합니다. 같은 타입이라도 Key가 다르면 Flutter는 이를 별개의 위젯으로 인식하고, 다른 상태를 유지하도록 처리합니다. 따라서 Key가 있는 위젯의 순서가 변경되면 Flutter는 새로운 위치에 있는 위젯의 상태를 유지하도록 관리합니다.
 
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