naeileun_pin_input 0.0.2
naeileun_pin_input: ^0.0.2 copied to clipboard
6자리 PIN(간편 비밀번호) 입력용 모달 바텀시트 위젯. 입력 완료 후 외부 검증 → controller로 success/error/close 제어.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:naeileun_pin_input/pin_input.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pin Input Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String? _enteredCode;
String? _message;
PinInputController? _controller;
late final VoidCallback _controllerListener;
bool _errorClearOnError = true;
int _errorRevertMs = 800;
String _errorMessage = '비밀번호가 올바르2지 않습니다.';
String _validCode = '123456';
@override
void initState() {
super.initState();
_controllerListener = () {
setState(() {
_enteredCode = _controller?.text;
});
};
}
@override
void dispose() {
_controller?.removeListener(_controllerListener);
_controller?.dispose();
super.dispose();
}
void _setController(PinInputController controller) {
_controller?.removeListener(_controllerListener);
_controller?.dispose();
_controller = controller;
_controller?.addListener(_controllerListener);
setState(() {
_enteredCode = _controller?.text;
});
}
void _openBottomSheet({
required String title,
required String description,
required String shuffleText,
PinInputTheme? theme,
bool showSkipButton = false,
}) {
setState(() {
_message = null;
_enteredCode = null;
});
final controller = PinInputController();
_setController(controller);
SimpleVerificationBottomSheet.show(
context: context,
controller: controller,
title: title,
description: description,
errorText: _errorMessage,
shuffleText: shuffleText,
theme: theme,
showSkipButton: showSkipButton,
titleStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.black),
descriptionStyle: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey.shade700,
height: 1.4,
),
numberTextStyle: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Colors.black,
),
buttonTextStyle: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
errorTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFFF44336),
),
dotStyle: PinDotStyle(
size: 14,
spacing: 10,
filledColor: (theme ?? PinInputTheme.defaultTheme()).domainColor,
emptyColor: (theme ?? PinInputTheme.defaultTheme()).domainColor.withOpacity(0.2),
errorFilledColor: const Color(0xFFF44336),
errorEmptyColor: const Color(0xFFF44336).withOpacity(0.2),
),
onInputComplete: (code) {
setState(() {
_message = '6자리 입력 완료: $code';
_enteredCode = code;
});
// 외부에서 검증 -> controller로 success/error 처리
if (code == _validCode) {
controller.success();
controller.close();
} else {
controller.error(
clearOnError: _errorClearOnError,
revertAfter: Duration(milliseconds: _errorRevertMs),
message: _errorMessage,
);
}
},
onSkip: () {
setState(() {
_message = '건너뛰기를 선택했습니다.';
});
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pin Input Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatusCard(),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_openBottomSheet(
title: '간편 비밀번호를 입력해 주세요',
description: '계정 정보를 안전하게 지키고,\n다음부터는 간편하게 로그인할 수 있어요.',
shuffleText: '재배열',
showSkipButton: false,
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: const Text('바텀시트 열기 (기본)', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_openBottomSheet(
title: 'PIN 코드를 입력하세요',
description: '6자리 숫자를 입력해주세요.',
shuffleText: '섞기',
showSkipButton: true,
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: const Text('바텀시트 열기 (건너뛰기 포함)', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_openBottomSheet(
title: '커스텀 테마 테스트',
description: '테마 색상을 커스터마이징할 수 있습니다.',
shuffleText: '재배열',
showSkipButton: true,
theme: const PinInputTheme(
domainColor: Color(0xFF9C27B0),
emptyOpacity: 0.18,
descriptionOpacity: 0.70,
textOpacity: 0.90,
buttonTextOpacity: 0.90,
),
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: const Text('커스텀 테마 테스트', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 24),
_buildControllerPanel(),
],
),
),
),
);
}
Widget _buildStatusCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('컨트롤러 상태', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
Text(
'controller: ${_controller == null ? 'null' : 'active'}',
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
),
const SizedBox(height: 4),
Text(
'controller.text: ${(_controller?.text.isNotEmpty ?? false) ? _controller!.text : '(empty)'}',
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
),
if (_message != null) ...[
const SizedBox(height: 8),
Text(_message!, style: TextStyle(fontSize: 14, color: Colors.grey.shade800)),
],
if (_enteredCode != null && _enteredCode!.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
'완료 코드: $_enteredCode',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
],
),
);
}
Widget _buildControllerPanel() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('컨트롤러 조작', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _controller == null ? null : () => _controller!.success(),
child: const Text('success()', style: TextStyle(fontSize: 14)),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: _controller == null
? null
: () => _controller!.error(
clearOnError: _errorClearOnError,
revertAfter: Duration(milliseconds: _errorRevertMs),
message: _errorMessage,
),
child: const Text('error()', style: TextStyle(fontSize: 14)),
),
),
],
),
const SizedBox(height: 12),
Text('검증용 정답 코드: $_validCode', style: const TextStyle(fontSize: 14)),
const SizedBox(height: 6),
TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '에러 메시지 (errorText / controller.error(message))',
),
onChanged: (v) => _errorMessage = v,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _controller == null ? null : () => _controller!.close(),
child: const Text('close()', style: TextStyle(fontSize: 14)),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: _controller == null ? null : () => _controller!.clear(),
child: const Text('clear()', style: TextStyle(fontSize: 14)),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _errorClearOnError,
onChanged: (v) {
setState(() {
_errorClearOnError = v ?? true;
});
},
),
Expanded(
child: const Text('error 시 입력 초기화(clearOnError)', style: TextStyle(fontSize: 14)),
),
],
),
const SizedBox(height: 4),
Row(
children: [
Text('revertAfter: $_errorRevertMs ms', style: const TextStyle(fontSize: 14)),
Expanded(
child: Slider(
value: _errorRevertMs.toDouble(),
min: 200,
max: 2000,
divisions: 18,
label: '$_errorRevertMs ms',
onChanged: (v) {
setState(() {
_errorRevertMs = v.round();
});
},
),
),
],
),
],
),
);
}
}