Flutter - 커스텀 위젯 생성 + 삭제 + 드래그 이동 구현

PINKIPPO·2023년 7월 20일
0

Flutter

목록 보기
2/5
post-thumbnail

저번시간에 작성한 드래그 위젯을 커스텀 위젯으로 바꿔 생성, 삭제하는 방법을 알려드리겠습니다

원래 내일 작성하려고 했는데 생각보다 쉽게 해결해서 같은 날에 작성하게 되었네요 ㅋㅋㅋㅋ
요즘 할일이 많아져서 오히려 좋다!! 저와 함께 플러터 실력을 늘려봅시다!!

예제 코드

이번에는 코드가 많이 길어져서 양해 부탁드립니다!


import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int widgetCount = 0; // 드래그 가능한 위젯의 Key 선언
  List<Widget> draggableWidgets = []; // 드래그 가능한 위젯 리스트 선언


  // draggableWidgets 삭제 메서드
  void removeWidget(Widget widget) {
    setState(() {
      draggableWidgets.remove(widget);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("드래그 연습"),
        centerTitle: true,
      ),
      body: Stack(
        children: [
          // 드래그 가능한 위젯들을 Spread Operator 이용해서 배치
          ...draggableWidgets,
        ],
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          FloatingActionButton(
            onPressed: () {
              // 버튼을 누르면 드래그 가능한 위젯 생성 후 리스트 추가
              setState(() {
                widgetCount++;
                //위젯에 key 추가 후 생성
                draggableWidgets.add(DraggableWidget(key: ValueKey(widgetCount),imageData: "assets/car.png"));
              });
            },
            child: SizedBox(
                width: 40,
                child: Image.asset("assets/car.png")),
          ),
          FloatingActionButton(
            onPressed: () {
              // 버튼을 누르면 드래그 가능한 위젯 생성 후 리스트 추가
              setState(() {
                widgetCount++;
                draggableWidgets.add(DraggableWidget(key: ValueKey(widgetCount),imageData : "assets/explosion.png"));
              });
            },
            child:  SizedBox(
                width: 40,
                child: Image.asset("assets/explosion.png")),
          ),
          FloatingActionButton(
            onPressed: () {
              // 버튼을 누르면 드래그 가능한 위젯 생성 후 리스트 추가
              setState(() {
                widgetCount++;
                draggableWidgets.add(DraggableWidget(key: ValueKey(widgetCount),imageData : "assets/person.png"));
              });
            },
            child: SizedBox(
                width: 40,
                child: Image.asset("assets/person.png")),
          ),
        ],
      ),
    );
  }
}


class DraggableWidget extends StatefulWidget {
  final String imageData;

  // 생성자 -> 키 + imageData
  const DraggableWidget({required Key key, required this.imageData}) : super(key: key);

  
  State<DraggableWidget> createState() => _DraggableWidgetState();
}

class _DraggableWidgetState extends State<DraggableWidget> {
  Offset offset = Offset(0, 0);

  
  Widget build(BuildContext context) {
    return Positioned(
      left: offset.dx,
      top: offset.dy,
      child: GestureDetector(
        child: Image.asset(widget.imageData),
        onPanUpdate: (details) {
          setState(() {
            offset = Offset(offset.dx + details.delta.dx, offset.dy + details.delta.dy);
          });
        },
        onLongPress: () {
          // 다이얼로그 생성 및 확인 버튼 누를 시 위젯 삭제
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text("위젯 삭제"),
                content: Text("이 위젯을 삭제하시겠습니까?"),
                actions: [
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop(); // 다이얼로그 닫기
                    },
                    child: Text("취소"),
                  ),
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop(); // 다이얼로그 닫기
                      _removeWidget(); // 위젯 삭제
                    },
                    child: Text("삭제"),
                  ),
                ],
              );
            },
          );
        },
      ),
    );
  }

  void _removeWidget() {
    // _MyAppState 클래스 찾아 변수 parent 할당 -> _MyAppState 클래스 메서드 호출 가능
    final parent = context.findAncestorStateOfType<_MyAppState>();
    // parent != null 일 때 removeWidget 메서드 호출
    parent?.removeWidget(widget);
  }

}

실행 화면

설명

음 저번 포스트에서 작성했던 Draggable 위젯을 버튼을 통해서 생성할 수 있게 커스텀 위젯으로 바꿔 재사용했습니다.
또 이미지를 꾹 눌렀을 때 삭제하는 기능도 추가했습니다.

기능적으로 어려운 부분은 없었으므로 코드에 대해서 자세하게는 설명하지 않겠습니다.
가능하면 주석 더 열심히 달겠습니다

하지만 코드를 수정하면서 한 가지 문제가 있었습니다.

플로팅 버튼을 통해서 여러개의 위젯을 생성한 상태에서 삭제하면 위젯들 위치가 섞이거나 의도하지 않은 오류가 발생하는 겁니다.

원인은 바로 Stateful Widget의 Key 문제였습니다. 이제 해당 오류를 어떻게 해결했는지 알아봅시다!!

트러블 슈팅 - StatefulWidget Key

문제 해결 방법을 적기전에 Key에 대해서 간단하게 알아봅시다!

위젯 트리(Widget Tree) & 엘리멘트 트리(Element Tree)

플러터의 모든 위젯은 @immutable 어노테이션을 가지고 있습니다. 불변하다는 의미입니다. 하지만 UI는 사용자와 수 많은
상호작용을 하며 데이터가 바뀝니다. 이를 해결하기 위해서 플러터에서는 위젯 트리를 통해 UI를 관리합니다.

플러터는 모든 widget마다 Element를 생성합니다. 이 안에 대응되는 widget의 타입 정보, 자식 Element 참조를 가지고 있습니다. 이제 위젯의 순서가 변경되거나 하면 플러터는 엘리멘트 트리를 살펴보며 구조가 변경되었는지 확인합니다.

하지만 StatefulWidget은 내부적인 Element 구조가 약간 다릅니다!

동일하게 위젯 트리와 엘리멘트 트리가 존재하지만 추가로 State Object가 따로 존재합니다.

제가 작성한 DraggableWidget()의 Offset도 State Object 입니다.

Stateful인 DraggableWidget()을 위치를 Key가 없는 상태에서 삭제 해봤다고 가정해보겠습니다.
행동이 완료되면 flutter는 엘리멘트 트리와 위젯 트리가 동일한지 검사합니다.
하지만 위젯 트리만 변경되고 엘리멘트 트리의 상태는 변하지않아 제대로 동작하지 않습니다.

비슷한 내용의 흐름을 나타낸 예시 이미지 입니다.

해결 방법

Stateful인 위젯에 Key를 추가하면 됩니다.

만약 위젯에 Key가 추가되면 검사가 진행되어도 위젯과 엘리멘트의 Key값이 달라지므로 차이를 발견할 수 있습니다. 그리고 Flutter는 불일치가 발견되면 일치되는 엘리먼트를 찾아 참조를 업데이트하고 화면이 변경됩니다.

이렇게 또 지식이 늘었다!! 열심히 공부해서 꼭 플러터를 마스터 하고 싶습니다 ㅋㅋㅋ
왜 공부하면 공부할수록 더 재미있는건데!! 스프링부트 미안...ㅜ

또 도움되거나 재미있는 내용 생기면 블로그 포스팅으로 찾아뵙겠습니다. 긴 글 읽어주셔서 감사합니다!!

profile
개발자가 될수있을까?

1개의 댓글

comment-user-thumbnail
2023년 7월 20일

가치 있는 정보 공유해주셔서 감사합니다.

답글 달기

관련 채용 정보