플러터 (flutter)

Flutter 위젯 생성 및 삭제

CreatoMaestro 2023. 7. 6. 20:44
반응형

저번 글에서는 사용자가 위젯을 드래그하면 그대로 위젯이 움직이도록 만들었다.

이번 글에서는 움직일 수 있는 위젯을 선택하여 추가, 삭제 할 수 있도록 할 것이다.

 

이 앱을 실행하면 다음과 같은 결과가 나온다.

앱 동작 영상

아래에 있는 네모들을 클릭하면 그 네모가 화면에 나온다.

아래 위젯은 스크롤이 가능하다.

 

네모는 드래그를 통해 이동이 가능하다.

네모를 터치하면 하안 선으로 감싸진다.

이 상태는 네모가 선택되어 있는 상태이다.

이 상태로 오른쪽 위에 있는 쓰레기통 버튼을 누르면 위젯이 삭제된다.

 

이번 앱에는 저번에 만든 앱을 기반으로 만들어진다.

위젯의 이동에 대해서는 저번 글에 참고하자.

2023.07.05 - [단기 프로젝트] - Flutter 화면에서 드래그로 위젯 이동시키기 (프로젝트1_2)

 

Flutter 화면에서 드래그로 위젯 이동시키기 (프로젝트1_2)

이번 글에서는 화면 안에서 위젯을 어떻게 이동시키는지 알아본다. 앱을 완성하면 다음과 같이 드래그로 위젯을 이동시킬 수 있다. 위젯을 움직이기 위해 이 앱에서는 Stack, Positioned, GestureDetector

ti-project-11.tistory.com

 

1. 추가 가능한 위젯 클래스 생성

우선 lib폴더 안에 adding_widget.dart 파일을 생성해준다.

파일 생성

이 파일 안에 클래스를 생성해준다.

 

import 'package:flutter/material.dart';

class AddingWidget extends StatefulWidget {
  final Color color;
  final String id;
  bool isSelected;

  AddingWidget(
      {required this.color,
      required this.id,
      required this.isSelected,
      Key? key})
      : super(key: key);

  @override
  State<AddingWidget> createState() => _AddingWidgetState();
}

다음 코드는 추가 가능한 위젯인 AddingWiget 클래스를 생성해주는 코드이다.

 

클래스 안에는 color 변수와 id 변수, 그리고 isSelected 변수가 있다.

color 변수는 선택한 컬러를 나타낸다. 

이 변수에 들어오는 컬러가 위젯의 색이다.

 

id는 위젯 고유의 아이디를 나타내고, isSelected는 이 위젯이 선택된 상태인지 나타낸다.

 

생성자를 이용해 이 변수들을 초기화해준다.

 

class _AddingWidgetState extends State<AddingWidget> {
  double positionX = 150;
  double positionY = 300;

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: positionX,
      top: positionY,
      child: Container(
        decoration: widget.isSelected
            ? BoxDecoration(
                color: widget.color,
                border: Border.all(
                  color: Colors.white,
                  width: 2.0,
                ),
              )
            : BoxDecoration(
                color: widget.color,
                border: Border.all(
                  color: Colors.transparent,
                  width: 2.0,
                ),
              ),
        width: 100,
        height: 100,
        child: GestureDetector(
          onTap: () {
            setState(() {
              widget.isSelected = !widget.isSelected;
              print(widget.isSelected);
            });
          },
          onScaleUpdate: (details) {
            setState(() {
              positionX += details.focalPointDelta.dx;
              positionY += details.focalPointDelta.dy;
            });
          },
        ),
      ),
    );
  }
}

다음은 AddingWidget 클래스의 상태 클래스이다.

상태 클래스에서는 실제 화면에 나타나는 위젯을 반환해준다.

 

 

Positioned 클래스 안에 Container 위젯을 넣었고, 그 안에 GestureDetector를 넣었다.

GestureDetector 에서 받아온 드래그 정보(details.focalPointDelta.dx,details.focalPointDelta.dy)들을 이용해 위치 정보를 업데이트한다.

 

이전에 만든 것과 다른 점은 onTap 파라미터가 추가된 것이다.

onTap은 위젯을 터치했을 때 실행되는 파라미터이다.

만약 위젯을 터치했을 때 선택되지 않은 상태이면 선택된 상태로 바꾸고, 선택된 상태면 선택되지 않은 상태로 바꾼다.

그 밑에 있는 print는 확인용이므로 없어도 무관하다.

 

자세한 것은 이전 글을 참고하자.

이 글에서는 위젯의 생성과 삭제에 집중한다.

 

2. main.dart 작성

main.dart에서는 위젯이 생성될 화면을 생성해준다.

 

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:widget_add/adding_widget.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData.dark(),
    home: Scaffold(
      body: BackScreen(),
    ),
  ));
}

필요한 라이브러리를 import해준다.

uuid는 위젯의 고유한 id를 생성해주기 위해 사용한다.

adding_widget.dart는 우리가 위에서 생성해준 파일이다.

 

main 함수의 runApp 안에 MaterialApp, Scaffold 위젯을 넣어주고 Scaffold의 body로 BackScreen을 넣어준다.

BackScreen은 기본 바탕이 되는 클래스이다.

 

지금부터는 BackScreen 클래스를 생성해준다.

class BackScreen extends StatefulWidget {
  List colors = [
    Colors.blueAccent,
    Colors.blue,
    Colors.deepPurple,
    Colors.tealAccent,
    Colors.purpleAccent,
    Colors.amberAccent,
    Colors.red,
    Colors.indigo
  ];

  BackScreen({Key? key}) : super(key: key);

  @override
  State<BackScreen> createState() => _BackScreenState();
}

다음 코드는 BackScreen 클래스를 생성하는 코드이다.

클래스 안에 있는 color 리스트는 아래에 위치한 스크롤을 생성하기 위한 리스트이다.

 

BackScreen({Key? key}):super(key: key)는 BackScreen 클래스의 인스턴스를 생성하는 생성자이다.

State<BackScreen>은 BackScreen의 상태를 나타낸다.

 

class _BackScreenState extends State<BackScreen> {
  Set<AddingWidget> widgets = {};
  Set<String> widgetId = {};

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        delete(),
        ...widgets,
        footer(),
      ],
    );
  }

다음은 BackScreenState의 상태 클래스인 _BackScreenState 클래스이다.

 

Set 타입의 변수 widgets은 AddingWidget을 담는 변수이다.

새로운 위젯이 생성되면 widgets 안에 담긴다.

Set은 리스트와 비슷한 역할을 하며, List와는 다르게 중복을 허용하지 않는다.

 

widgetId는 생성된 위젯의 id를 저장하는 Set 타입 변수이다.

 

build 함수에서는 Stack 위젯을 반환해준다.

chidren으로는 delete함수에서 반환한 위젯, widgets 안에 있는 위젯, footer에서 반환하는 위젯이 들어간다.

...widgets는 widgets 안에 있는 요소들을 의미한다.

 

이제부터는 각각의 함수에 대해 살펴본다.

Widget delete() {
  return Positioned(
    right: 0,
    top: 10,
    child: IconButton(
        iconSize: 40.0,
        onPressed: () {
          setState(() {
            widgets = widgets
                .where((element) => element.isSelected != true)
                .toSet();
          });
        },
        icon: Icon(Icons.delete_forever_outlined)),
  );
}

이 함수는 터치 시 선택되어 있는 위젯을 삭제하는 아이콘 버튼을 반환한다.

Positioned 함수를 통해 위치는 오른쪽 위로 조정해준다.

 

버튼이 눌렸을 때 실행되는 onPressed에 난해한 코드가 하나 있다.

이 코드에 대해 살펴보자.

 

widgets는 Set 타입의 변수이다.

Set 타입의 변수는 where이라는 메서드를 가지고 있다.

where 메서드는 변수 안에 있는 요소를 하나하나 검사하고, 이 중에서 일치하는 요소만 반환해 저장해준다.

그렇기 때문에 isSelected != true라고 하면 선택되지 않은 요소만 남게 된다.

이후 toSet()을 이용해 다시 Set 타입의 변수로 바꿔준다.

 

이 과정은 setState()함수 안에 있다.

그렇기 때문에 과정이 끝나면 위젯을 다시 빌드하여 남은 위젯만 화면에 띄우게 된다.

 

다음은 footer함수이다.

Widget footer() {
  return Positioned(
      left: 0,
      right: 0,
      bottom: 0,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
          children: widget.colors
              .map((e) => GestureDetector(
                    onTap: () {
                      String id = Uuid().v4();
                      setState(() {
                        widgetId.add(id);
                        widgets.add(AddingWidget(
                          color: e,
                          id: id,
                          isSelected: false,
                        ));
                      });
                    },
                    child: Container(
                      width: 100,
                      height: 100,
                      color: e,
                    ),
                  ))
              .toList(),
        ),
      ));

이 함수는 스크롤 가능한 선택 창을 반환한다.

Positioned 위젯을 이용해 밑에 위치하도록 하였고, 양 옆을 모두 차지하도록 만들었다.

 

SingleChildScrollView를 이용해 스크롤이 가능하도록 만들었고, 좌우로 스크롤이 가능하도록 하였다.

선택 가능한 위젯들은 Row를 통해 정렬하였다.

 

widget.colors
    .map((e) => GestureDetector(
          onTap: () {
            String id = Uuid().v4();
            setState(() {
              widgetId.add(id);
              widgets.add(AddingWidget(
                color: e,
                id: id,
                isSelected: false,
              ));
            });
          },
          child: Container(
            width: 100,
            height: 100,
            color: e,
          ),
        ))
    .toList(),

children 안에 있는 이 코드가 이해하기 어려울 수도 있다.

이 코드는 BackScreen 클래스 안에 있는 색 리스트를 이용해 터치 가능한 Container 위젯을 생성하는 코드이다.

widget.color는 BackScreen에 있는 컬러 리스트를 의미하고,

.map메서드는 Set 안에 있는 모든 요소를 이용해 특정 코드를 수행하는 코드이다.

반환이 가능하기 때문에 우리가 원하는 위젯을 반환해줄 수 있다.

 

.map((e) => GestureDetector( ....) : 이 코드는 Set 안에 있는 요소를 파라미터로 받고 (e 요소), 그 다음 그 요소를 이용해 GestureDetector를 반환한다.

GestureDetector의 child로는 Container를 넣어준다.

width와 height은 100으로 해주고, color는 파라미터로 받은 e 값을 넣어준다.

 

GestureDetector의 onTap의 Callback 함수 안에는 setState가 있다.

SetState 함수 안에서 uuid를 통해 id를 생성해주고, 그 아이디를 widgetId에 add 메서드를 통해 넣어준다.

그리고 widgets 안에 AddingWidget을 추가해준다.

AddingWidget의 색은 e 값이고, id는 uuid를 통해 생성해준 id를 넣어준다.

isSelected는 false로 초기화해준다.

 

모든 것을 해준 뒤에는 위젯을 다시 build해 업데이트 된 값으로 다시 화면을 생성해준다.

 

이렇게 하면 모든 코드가 완성된다.

 

아래는 전체 코드이다.

1)adding_widget.dart

import 'package:flutter/material.dart';

class AddingWidget extends StatefulWidget {
  final Color color;
  final String id;
  bool isSelected;

  AddingWidget(
      {required this.color,
      required this.id,
      required this.isSelected,
      Key? key})
      : super(key: key);

  @override
  State<AddingWidget> createState() => _AddingWidgetState();
}

class _AddingWidgetState extends State<AddingWidget> {
  double positionX = 150;
  double positionY = 300;

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: positionX,
      top: positionY,
      child: Container(
        decoration: widget.isSelected
            ? BoxDecoration(
                color: widget.color,
                border: Border.all(
                  color: Colors.white,
                  width: 2.0,
                ),
              )
            : BoxDecoration(
                color: widget.color,
                border: Border.all(
                  color: Colors.transparent,
                  width: 2.0,
                ),
              ),
        width: 100,
        height: 100,
        child: GestureDetector(
          onTap: () {
            setState(() {
              widget.isSelected = !widget.isSelected;
            });
          },
          onScaleUpdate: (details) {
            setState(() {
              positionX += details.focalPointDelta.dx;
              positionY += details.focalPointDelta.dy;
            });
          },
        ),
      ),
    );
  }
}

 

2)main.dart

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:widget_add/adding_widget.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData.dark(),
    home: Scaffold(
      body: BackScreen(),
    ),
  ));
}

class BackScreen extends StatefulWidget {
  List colors = [
    Colors.blueAccent,
    Colors.blue,
    Colors.deepPurple,
    Colors.tealAccent,
    Colors.purpleAccent,
    Colors.amberAccent,
    Colors.red,
    Colors.indigo
  ];

  BackScreen({Key? key}) : super(key: key);

  @override
  State<BackScreen> createState() => _BackScreenState();
}

class _BackScreenState extends State<BackScreen> {
  Set<AddingWidget> widgets = {};
  Set<String> widgetId = {};

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        delete(),
        ...widgets,
        footer(),
      ],
    );
  }

  Widget delete() {
    return Positioned(
      right: 0,
      top: 10,
      child: IconButton(
          iconSize: 40.0,
          onPressed: () {
            setState(() {
              widgets = widgets
                  .where((element) => element.isSelected != true)
                  .toSet();
            });
          },
          icon: Icon(Icons.delete_forever_outlined)),
    );
  }


  Widget footer() {
    return Positioned(
        left: 0,
        right: 0,
        bottom: 0,
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: widget.colors
                .map((e) => GestureDetector(
                      onTap: () {
                        String id = Uuid().v4();
                        setState(() {
                          widgetId.add(id);
                          widgets.add(AddingWidget(
                            color: e,
                            id: id,
                            isSelected: false,
                          ));
                        });
                      },
                      child: Container(
                        width: 100,
                        height: 100,
                        color: e,
                      ),
                    ))
                .toList(),
          ),
        ));
  }
}

 

3.추가 내용(수정 사항)

글을 쓴 후에 먼저 생성된 위젯을 삭제할 경우, 나중에 생성된 위젯의 위치가 바뀌는 오류가 있음을 알았다.

 

이를 해결하기 위해 위치를 저장하는 변수를 상태 클래스가 아니라 StatefulWidget 클래스에 지정해주었다.

setState를 통해 build를 다시 하면서 값이 바뀐 것 같다.

 

adding_widget.dart 안에 코드가 다음과 같이 바뀌였다.

class AddingWidget extends StatefulWidget {
  final Color color;
  final String id;
  bool isSelected;

  double _positionX = 150;
  double _positionY = 300;


  AddingWidget(
      {required this.color,
      required this.id,
      required this.isSelected,
      Key? key})
      : super(key: key);

  @override
  State<AddingWidget> createState() => _AddingWidgetState();
}

class _AddingWidgetState extends State<AddingWidget> {

  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: widget._positionX,
      top: widget._positionY,
      child: Container(
      
      ....
반응형