한국어
오픈소스포럼
 이곳은 다양한 오픈소스 프로젝트를 소개하고 리뷰, 활용 방법을 공유합니다.

Flutter 위젯의 상태관리에 대해서

makersweb 2023.04.06 11:13 조회 수 : 447

이 글에서는 Flutter에서 위젯의 상태관리에 대해서 몇 가지 기본적인 접근방법을 소개하고 있다.

TL;DR:

  • 상태를 관리하는 방법에는 여러 가지가 있다.
  • 위젯 디자이너는 사용할 접근 방식을 선택한다.
  • 확실하지 않은 경우 상위 위젯에서 상태를 관리하는 것이 좋다.

stateful 위젯의 상태는 누가 관리할까? 위젯 자체? 상위 위젯? 둘 다? 제3객체? 답은… 상황에 따라 다르다. 위젯을 대화형으로 만드는 몇 가지 유효한 방법이 있다. 위젯을 설계할 때 위젯이 어떻게 사용될 것으로 예상하는지에 따라 결정을 내린다. 상태를 관리하는 가장 일반적인 방법은 다음과 같다.

  • 위젯 자체에서 상태를 관리
  • 부모에서 위젯의 상태를 관리
  • 위 두가지를 적절히 혼합

어떤 접근 방식을 사용할지 어떻게 결정할까? 다음 원칙은 결정하는 데 도움이 된다.

  • 상태가 사용자 데이터인 경우(예: 확인란의 선택 또는 선택 해제 모드 또는 슬라이더의 위치) 상태는 상위 위젯에서 가장 잘 관리된다.

  • 상태가 유동적인 경우(예: 애니메이션) 위젯 자체에서 상태를 관리하는 것이 가장 좋다.

확실하지 않은 경우 상위 위젯에서 상태를 관리하는 것부터 시작하는 것이 좋다.

예제

TapboxA, TapboxB 및 TapboxC의 세 가지 간단한 예를 만들어 상태를 관리하는 다양한 방법의 예를 살펴본다.

예제는 모두 유사하게 작동한다. 각 예제는 탭할 때 녹색 또는 회색 상자 사이를 전환하는 컨테이너를 만든다. boolean의 _active은 색상을 결정한다. 녹색은 활성, 회색은 비활성이다.

tapbox-active-state.pngtapbox-inactive-state.png

이 예제에서는 GestureDetector를 사용하여 Container에서 활동을 캡처한다.

위젯 자체에서 상태를 관리

때로는 위젯이 내부적으로 상태를 관리하는 것이 가장 합리적이다. 예를 들어 ListView는 콘텐츠가 렌더링 상자를 초과하면 자동으로 스크롤된다. ListView를 사용하는 대부분의 개발자는 ListView의 스크롤 동작을 관리하기를 원하지 않으므로 ListView 자체에서 스크롤 오프셋을 관리한다.

_TapboxAState 클래스의 역할:

  • TapboxA의 상태를 관리한다.
  • 상자의 현재 색상을 결정하는 _active 부울을 정의한다.
  • 상자를 누를 때 _active를 업데이트하고 setState() 함수를 호출하여 UI를 업데이트하는 _handleTap() 함수를 정의한다.
  • 위젯에 대한 모든 대화형 동작을 구현한다.
import 'package:flutter/material.dart';

// TapboxA manages its own state.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  const TapboxA({super.key});

  @override
  State<TapboxA> createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo'),
        ),
        body: const Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}

위젯의 상태를 상위 위젯이 관리

종종 부모 위젯이 상태를 관리하고 자식 위젯에 업데이트 시기를 알려주는 것이 가장 합리적일 수 있다. 예를 들어 IconButton을 사용하면 아이콘을 탭 가능한 버튼으로 취급할 수 있다. IconButton은 적절한 조치를 취할 수 있도록 부모 위젯이 버튼이 탭되었는지 여부를 알아야 한다고 결정했기 때문에 Stateless 위젯이다.

다음 예에서 TapboxB는 콜백을 통해 상태를 부모에게 내보낸다. TapboxB는 상태를 관리하지 않기 때문에 StatelessWidget의 하위 클래스다.

ParentWidgetState 클래스의 역할:

  • TapboxB의 _active 상태를 관리한다.
  • 상자를 탭할 때 호출되는 메서드인 _handleTapboxChanged()를 구현한다.
  • 상태가 변경되면 setState()를 호출하여 UI를 업데이트한다.

TapboxB 클래스의 역할:

  • 모든 상태가 부모에 의해 처리되기 때문에 StatelessWidget을 확장하여 구현된다.
  • 탭이 감지되면 부모에게 알린다.
import 'package:flutter/material.dart';

// ParentWidget manages the state for TapboxB.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  const TapboxB({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

적절히 혼합(Mix and match)한 방식

일부 위젯의 경우 믹스 앤 매치 방식이 가장 적합하다. 이 시나리오에서 상태 저장 위젯은 일부 상태를 관리하고 상위 위젯은 상태의 다른 측면을 관리한다.

TapboxC 예제에서 아래로 탭하면 상자 주위에 진한 녹색 테두리가 나타난다. 위로 탭하면 테두리가 사라지고 상자의 색상이 변경된다. 위로 탭하면 테두리가 사라지고 상자의 색상이 변경된다. TapboxC는 _active 상태를 부모에게 내보내지만 _highlight 상태는 내부적으로 관리한다. 이 예제에는 _ParentWidgetState 및 _TapboxCState라는 두 개의 State 객체가 있다.

_ParentWidgetState 객체의 역할:

  • _active 상태를 관리한다.
  • 상자를 탭할 때 호출되는 메서드인 _handleTapboxChanged()를 구현한다.
  • 탭이 발생하고 _active 상태가 변경되면 setState()를 호출하여 UI를 업데이트한다.

_TapboxCState 객체의 역할:

  • _highlight 상태를 관리한다.
  • GestureDetector는 모든 탭 이벤트를 수신한다. 사용자가 탭하면 하이라이트 표시가 추가된다(진한 녹색 테두리로 구현됨). 사용자가 탭을 놓으면 하이라이트가 제거된다.
  • setState()를 호출하여 탭 다운, 탭 업 또는 탭 취소 시 UI를 업데이트하고 _highlight 상태가 변경된다.
  • 탭 이벤트에서 해당 상태 변경을 부모 위젯에 전달하여 widget 속성을 사용하여 적절한 조치를 취한다.
import 'package:flutter/material.dart';

//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  const TapboxC({
    super.key,
    this.active = false,
    required this.onChanged,
  });

  final bool active;
  final ValueChanged<bool> onChanged;

  @override
  State<TapboxC> createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  @override
  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700]!,
                  width: 10.0,
                )
              : null,
        ),
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: const TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
      ),
    );
  }
}

대체 구현으로 활성 상태를 내부에 유지하면서 highlight 표시 상태를 부모에게 내보냈을 수 있지만 누군가에게 해당 탭 상자를 사용하도록 요청하면 별로 의미가 없다고 불평할 것이다. 개발자는 상자가 활성화되어 있는지 여부를 확인해야 한다. 개발자는 highlight 표시 관리 방법에 관심이 없으며 탭 상자에서 이러한 세부 정보를 처리하는 것을 선호할 수 있다.

번호 제목 글쓴이 날짜 조회 수
35 Pluma(C++ Plug-in Management Framework) 튜토리얼 file makersweb 2019.12.07 13975
34 GDBus 튜토리얼(GDBus tutorial) file makersweb 2019.06.30 10323
33 Dear ImGui, 경량의 C++ 용 GUI 및 Widget 라이브러리 file makersweb 2020.11.28 9033
32 ZeroMQ의 기본 메세지 패턴들 file makersweb 2020.07.31 8470
31 GENIVI DLT(Diagnostic Log and Trace) 활용 file makersweb 2020.11.19 8263
30 텔레그램(Telegram) Bot 개발 file makersweb 2019.07.21 5702
29 webOS소개 및 Raspberry Pi 3 에서 실행 file makersweb 2019.10.13 3871
28 가볍고 쉬운 임베디드용 그래픽 라이브러리 - LVGL file makersweb 2020.09.16 3309
27 [SDL2 와 OpenGL]윈도우 생성과 2D그래픽 file makersweb 2020.04.11 3053
26 리눅스에서 SDL2 최신버전 컴파일과 Qt Creator로 개발환경 구성 file makersweb 2019.10.06 3028
25 Flutter Application 에서 한글(EUC-KR) 깨져서 나오는 문제 file makersweb 2022.01.06 2666
24 Wayland IVI Extension 간단 리뷰 file makersweb 2019.05.12 2256
23 Nana, C++용 크로스플랫폼 GUI 라이브러리 file makersweb 2021.01.06 2063
22 AGL (Automotive Grade Linux) 개요 file makersweb 2022.06.19 1851
21 ZeroMQ 비동기 클라이언트/서버 패턴 file makersweb 2020.08.13 1761
20 CANdevStudio 를 사용하여 CAN 네트워크 시뮬레이션 file makersweb 2021.03.09 1688
19 Flutter/Dart 와 Qt/QML 비교 file makersweb 2021.11.07 1399
18 Qt와 GStreamer 로 작성한 flac 오디오 재생 예제 file makersweb 2020.09.05 1139
17 LVGL 을 통해 GUI 구현 시 한글 폰트 추가 file makersweb 2023.02.07 1067
16 [NodeGui] JavaScript로 데스크탑 응용프로그램 작성 file makersweb 2023.02.21 1064