[Flutter] 커스텀 라디오 버튼 리스트

현업에선 라디오 버튼 리스트를 개발해야하 하는 순간이 많다. 그리고 실무에선 거의 대부분 패딩이나 여백등 여러가지 디자인 요소로 인해 라디오 버튼 리스트를 커스텀해서 사용해야한다. 생각보다 시간이 많이걸리는 데, 그럴 때를 대비해 예제를 작성해보았다.

RadioBtnList 클래스

아래와 같이 라디오 버튼 리스트 클래스를 작성했다. radioOptions Map를 이용하고 onChangedFunc 콜백함수를 라디오 아이템 클래스에 인자로 넘겨줘서, 같은 코드가 반복되지 않도록 했다.

class RadioBtnList extends StatefulWidget {
  const RadioBtnList({super.key, required this.title});

  final String title;

  @override
  State<RadioBtnList> createState() => _RadioBtnListState();
}

class _RadioBtnListState extends State<RadioBtnList> {
  int selectedIdx = 0;
  final List<Map<String, dynamic>> radioOptions = [
    {'value': 1, 'label': 'Option 1'},
    {'value': 2, 'label': 'Option 2'},
    {'value': 3, 'label': 'Option 3'},
    {'value': 4, 'label': 'Option 4'},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: radioOptions.map((option) {
            return RadioButtonItem(
              value: option['value'],
              groupValue: selectedIdx,
              onChanged: onChangedFunc,
              label: option['label'],
            );
          }).toList(),
        ),
      ),
    );
  }

  onChangedFunc(int? i) {
    if (i == null) return;
    setState(() {
      selectedIdx = i;
    });
  }
}

RadioButtonItem 클래스

groupValue는 선택된 값이고, value는 개별 라디오 버튼의 값이다. groupValue와 value의 값이 일치할 때 라디오 버튼이 선택된다. Radio클래스의 onChanged 프로퍼티에만 콜백함수를 넣어주면 라벨을 탭 했을 땐, 콜백함수가 실행되지 않기 때문에 전체를 GestureDetector로 한번 더 감싸줘서 onTap에 onChanged 콜백을 넣어 주었다.

class RadioButtonItem extends StatelessWidget {
  final int value;
  final int groupValue;
  final void Function(int? i) onChanged;
  final String label; // 라벨 텍스트 매개변수 추가

  const RadioButtonItem({
    super.key,
    required this.value,
    required this.groupValue,
    required this.onChanged,
    required this.label, // 생성자에 라벨 추가
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        onChanged(value);
      },
      child: Row(
        children: [
          Radio(
            value: value,
            groupValue: groupValue,
            onChanged: onChanged,
          ),
          Text(
            label, // 라벨 텍스트 사용
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ],
      ),
    );
  }
}

전체 코드

아래 코드를 그대로 복사해서 예제코드로 동작을 확인해 볼 수 있다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Radio Buttons Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home:
          const RadioBtnList(title: 'Custom Radio Buttons Example'),
    );
  }
}

class RadioBtnList extends StatefulWidget {
  const RadioBtnList({super.key, required this.title});

  final String title;

  @override
  State<RadioBtnList> createState() => _RadioBtnListState();
}

class _RadioBtnListState extends State<RadioBtnList> {
  int selectedIdx = 0;
  final List<Map<String, dynamic>> radioOptions = [
    {'value': 1, 'label': 'Option 1'},
    {'value': 2, 'label': 'Option 2'},
    {'value': 3, 'label': 'Option 3'},
    {'value': 4, 'label': 'Option 4'},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: radioOptions.map((option) {
            return RadioButtonItem(
              value: option['value'],
              groupValue: selectedIdx,
              onChanged: onChangedFunc,
              label: option['label'],
            );
          }).toList(),
        ),
      ),
    );
  }

  onChangedFunc(int? i) {
    if (i == null) return;
    setState(() {
      selectedIdx = i;
    });
  }
}

class RadioButtonItem extends StatelessWidget {
  final int value;
  final int groupValue;
  final void Function(int? i) onChanged;
  final String label; // 라벨 텍스트 매개변수 추가

  const RadioButtonItem({
    super.key,
    required this.value,
    required this.groupValue,
    required this.onChanged,
    required this.label, // 생성자에 라벨 추가
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        onChanged(value);
      },
      child: Row(
        children: [
          Radio(
            value: value,
            groupValue: groupValue,
            onChanged: onChanged,
          ),
          Text(
            label, // 라벨 텍스트 사용
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ],
      ),
    );
  }
}