Coding/Dart & Flutter

[Flutter] Input Buttons & Widget

햇썽이 2026. 3. 5. 17:45

Input 위젯 대표적인 5가지

  • Checkbox: 여러 개 항목을 true/false로 선택
  • Radio: 여러 옵션 중 1개만 선택
  • Slider: 0~100 같은 연속 값(또는 구간 값)을 드래그로 선택
  • Switch / CupertinoSwitch: on/off 토글
  • PopupMenuButton: 메뉴에서 항목을 선택

// - Checkbox: 다중 선택
// - Radio: 단일 선택
// - Slider: 연속 값 선택
// - Switch/CupertinoSwitch: ON/OFF 토글
// - PopupMenuButton: 메뉴 선택

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  // 앱 실행 진입점
  runApp(
    MaterialApp(
      // MaterialApp은 테마, 라우팅 등 머티리얼 앱 전반 설정을 담당
      home: Scaffold(
        // Scaffold는 기본 화면 레이아웃(앱바, 바디 등)을 제공
        appBar: AppBar(
          title: Text("Let's Study Container"),
        ),
        body: Body(),
      ),
    ),
  );
}

/// 화면에 여러 입력 위젯 데모를 세로로 배치하는 위젯
class Body extends StatelessWidget {
  const Body({super.key});

  @override
  Widget build(BuildContext context) {
    // Column: 자식 위젯들을 세로로 배치
    return const Column(
      children: [
        TestCheckBox(),
        TestRadioButton(),
        TestSlider(),
        TestSwitch(),
        TestPopupMenu(),
      ],
    );
  }
}

// ------------------------------------------------------------
// 1) Checkbox (다중 선택)
// ------------------------------------------------------------

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

  @override
  State<TestCheckBox> createState() => _TestCheckBoxState();
}

class _TestCheckBoxState extends State<TestCheckBox> {
  // 체크 상태를 저장하는 리스트
  // 예: 3개의 체크박스 상태를 [false, false, false]로 관리
  late List<bool> values;

  @override
  void initState() {
    super.initState();

    // initState는 위젯이 처음 생성될 때 딱 1번 호출됨
    // 여기서 상태 초기화를 해두면 build마다 초기화되는 실수를 방지할 수 있음
    values = [false, false, false];
  }

  @override
  Widget build(BuildContext context) {
    // Row: 자식 위젯들을 가로로 배치
    return Row(
      children: [
        Checkbox(
          // 현재 체크 상태
          value: values[0],

          // 사용자가 체크/해제할 때 호출되는 콜백
          // value는 bool? (nullable)로 들어오므로 안전 처리 필요
          onChanged: (value) => changeValue(0, value: value),
        ),
        Checkbox(
          value: values[1],
          onChanged: (value) => changeValue(1, value: value),
        ),
        Checkbox(
          value: values[2],
          onChanged: (value) => changeValue(2, value: value),
        ),
      ],
    );
  }

  /// 특정 index의 체크 상태를 갱신하는 함수
  ///
  /// - setState 내부에서 상태를 변경해야 화면이 다시 렌더링됨
  /// - Checkbox의 onChanged는 bool?을 전달하기 때문에 파라미터를 nullable로 받음
  void changeValue(int index, {bool? value = false}) {
    setState(() {
      // value가 null일 가능성에 대비해서 !로 단언
      // (실전에서는 value ?? false 같이 널 병합 연산자를 더 자주 사용함)
      values[index] = value!;
    });
  }
}

// ------------------------------------------------------------
// 2) Radio (단일 선택)
// ------------------------------------------------------------

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

  @override
  State<TestRadioButton> createState() => _TestRadioButtonState();
}

/// 라디오 버튼에서 선택 가능한 값들을 enum으로 정의
///
/// - enum을 쓰면 문자열/정수로 관리할 때보다 안정적으로 선택값을 다룰 수 있음
enum TestValue {
  test1,
  test2,
  test3,
}

class _TestRadioButtonState extends State<TestRadioButton> {
  // 현재 선택된 값
  // null이면 아직 아무것도 선택되지 않은 상태
  TestValue? selectValue;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ListTile을 활용하면 라디오 + 텍스트 + 탭 영역을 한 줄로 구성하기 편함
        ListTile(
          leading: Radio<TestValue>(
            // 해당 Radio가 대표하는 값
            value: TestValue.test1,

            // 현재 그룹에서 선택된 값 (이 값과 value가 같으면 체크됨)
            groupValue: selectValue,

            // 라디오를 직접 눌렀을 때 실행
            onChanged: (value) => setState(() {
              selectValue = value!;
            }),
          ),

          // enum의 name을 사용하면 "test1" 같은 문자열을 바로 얻을 수 있음
          title: Text(TestValue.test1.name),

          // 타일 전체를 탭해도 선택되게 만들기
          onTap: () => setState(() {
            if (selectValue != TestValue.test1) {
              selectValue = TestValue.test1;
            }
          }),
        ),

        Radio<TestValue>(
          value: TestValue.test2,
          groupValue: selectValue,
          onChanged: (value) => setState(() {
            selectValue = value!;
          }),
        ),

        Radio<TestValue>(
          value: TestValue.test3,
          groupValue: selectValue,
          onChanged: (value) => setState(() {
            selectValue = value!;
          }),
        ),
      ],
    );
  }
}

// ------------------------------------------------------------
// 3) Slider (연속 값 선택)
// ------------------------------------------------------------

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

  @override
  State<TestSlider> createState() => _TestSliderState();
}

class _TestSliderState extends State<TestSlider> {
  // 슬라이더 값은 double
  // (Slider 위젯이 double 기반)
  double value = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 현재 값을 화면에 표시 (반올림해서 정수처럼 보이도록)
        Text('${value.round()}'),

        Slider(
          // 현재 값
          value: value,

          // 드래그해서 값이 바뀔 때마다 호출
          onChanged: (newValue) => setState(() => value = newValue),

          // divisions를 지정하면 "구간" 단위로 끊기는 슬라이더가 됨
          // 0~100을 100분할 → 사실상 정수 단위처럼 사용 가능
          divisions: 100,

          // 최댓값/최솟값
          max: 100,
          min: 0,

          // 사용자가 드래그 중일 때 보이는 라벨
          label: value.round().toString(),
        ),
      ],
    );
  }
}

// ------------------------------------------------------------
// 4) Switch / CupertinoSwitch (토글)
// ------------------------------------------------------------

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

  @override
  State<TestSwitch> createState() => _TestSwitchState();
}

class _TestSwitchState extends State<TestSwitch> {
  // 토글 상태 (on/off)
  bool value = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Material 디자인 Switch
        Switch(
          value: value,
          onChanged: (newValue) => setState(() => value = newValue),
        ),

        // iOS 스타일 Switch (Cupertino)
        // 같은 value를 공유해서 두 스위치가 동시에 동일 상태를 반영
        CupertinoSwitch(
          value: value,
          onChanged: (newValue) => setState(() => value = newValue),
        ),
      ],
    );
  }
}

// ------------------------------------------------------------
// 5) PopupMenuButton (메뉴 선택)
// ------------------------------------------------------------

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

  @override
  State<TestPopupMenu> createState() => _TestPopupMenuState();
}

class _TestPopupMenuState extends State<TestPopupMenu> {
  // 기본 선택값을 test1로 설정
  TestValue selectValue = TestValue.test1;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 현재 선택된 값을 텍스트로 표시
        Text(selectValue.name),

        // PopupMenuButton을 누르면 드롭다운 메뉴가 뜸
        PopupMenuButton<TestValue>(
          // 메뉴 아이템 구성
          itemBuilder: (context) {
            // enum 전체 값을 순회하면서 메뉴 아이템 리스트 생성
            return TestValue.values
                .map(
                  (value) => PopupMenuItem<TestValue>(
                    value: value,
                    child: Text(value.name),
                  ),
                )
                .toList();
          },

          // 사용자가 메뉴에서 항목을 선택하면 호출
          onSelected: (newValue) => setState(() => selectValue = newValue),
        ),
      ],
    );
  }
}

 

 

 

'Coding > Dart & Flutter' 카테고리의 다른 글

[Flutter] Stack  (0) 2026.03.03
[Flutter] Flexible, Expanded  (0) 2026.03.02
[Flutter] Stateful & Stateless  (0) 2026.02.26
[Flutter] Row, Column  (0) 2026.02.26
[Flutter] Container 세부 Function 확인해보기  (0) 2026.02.25