저번 시간에 이렇게 커스텀 키보드를 완성했다! 그러나 이걸 어떻게 이런 방식으로 구현했는지 코드를 이해해야 내가 원하는 방식으로 적용할 수 있기 때문에, 코드를 분석해볼 것이다.
코드를 분석한 다음, 현재 추가되어 있는 qr 다음 화면과 합치고, 현재 내가 개발 예정인 우리 어플 화면과 얼추 비슷하게 맞춰볼 예정이다.
저번에 말했듯이, 이 키보드는 키와 키보드 view가 나눠져 있는 형태로 구성되어 있다. 일단 키의 구조부터 살펴보자.
우선 전체 코드는 다음과 같다. 여기서 요소들을 하나하나 뜯어서 분석을 진행해보자.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class KeyboardKey extends StatefulWidget {
final dynamic label;
final dynamic value; //값
final ValueSetter<dynamic> onTap;
KeyboardKey({
required this.label,
required this.value,
required this.onTap,
});
@override
State<KeyboardKey> createState() => _KeyboardKeyState();
}
class _KeyboardKeyState extends State<KeyboardKey> {
renderLabel(){
if(widget.label is Widget){
return widget.label;
}
return Text(
widget.label,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
widget.onTap(widget.value);
},
child: Container(
width :123.429,
height: 60,
child: AspectRatio(
aspectRatio: 2,
child: Center(
child : renderLabel(),
),
),
),
);
}
}
class KeyboardKey extends StatefulWidget {
final dynamic label;
final dynamic value; //값
final ValueSetter<dynamic> onTap;
KeyboardKey({
required this.label,
required this.value,
required this.onTap,
});
@override
State<KeyboardKey> createState() => _KeyboardKeyState();
}
그동안 봤던 StatefulWidget이 state만을 정의하는 것과는 다르게, 이 속성은 key를 가지고 있다. 보통 StatefulWidget 클래스는 위젯의 상태와 관련된 데이터를 포함하거나 상태 변경을 트리거하는 콜백을 정의하는 역할을 한다.
그런데 이 코드에서는 StatefulWidget 자체가 단순히 상태를 관리하는 역할을 넘어서, 특정 키보드 키를 표현하는 데 사용되고 있다. 즉, 위젯 자체가 키를 표현하고, 해당 키에 대한 라벨, 값 및 탭 이벤트를 처리하기 위한 것이므로 이러한 속성을 StatefulWidget 클래스 내부에 직접 정의하는 것이 적합하다.
즉, 이 코드는 키보드 키를 표현하는 완전한 위젯으로써, 위젯 자체가 가지는 속성들을 분명히하고 상태와 연결된 부분을 구현함으로써 모듈화와 재사용성을 높이는 것을 목표로 하고 있습니다.즉, 상태와 관련된 부분을 state로 분리하고 키를 나타내는 요소들을 넣은 것이다.
class _KeyboardKeyState extends State<KeyboardKey> {
renderLabel(){
if(widget.label is Widget){
return widget.label;
}
return Text(
widget.label,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
widget.onTap(widget.value);
},
child: Container(
width :123.429,
height: 60,
child: AspectRatio(
aspectRatio: 2,
child: Center(
child : renderLabel(),
),
),
),
);
}
}
참고로 말하자면, State 클래스 내에서 build 메서드를 정의하는 것은 StatefulWidget이 현재 상태에 따라 화면을 재구성하고 다시 그리는 방법을 지정하는 것이다.
Flutter는 위젯 트리가 변경되면, 그에 따라 화면이 다시 그려진다. 이러한 변경은 상태가 변경될 때, 사용자 입력이 발생했을 때, 또는 애니메이션 등의 이벤트가 발생했을 때 발생할 수 있다. 이 때, StatefulWidget의 상태가 변경되면 해당 상태를 가지고 있는 State 클래스의 build 메서드가 호출되어 화면을 다시 그린다.
따라서 State 클래스 내의 build 메서드는 해당 상태에 따라 위젯을 어떻게 구성하고 표시할지를 결정한다.
정리하자면, InkWell을 포함하여 터치 효과를 추가하고, 키보드 키를 나타내는 컨테이너를 생성하며, 그 안에 라벨(키 값)을 중앙에 배치하는 위젯을 생성합니다.
다음은 이 키를 이용한 커스텀 키보드를 분석해보자.
일단 전체 코드는 다음과 같다.
import 'package:custom_keyboard_test/CustomKeyboard/KeyboardKey.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
class CustomKeyboard extends StatefulWidget {
const CustomKeyboard({super.key});
@override
State<CustomKeyboard> createState() => _CustomKeyboardState();
}
class _CustomKeyboardState extends State<CustomKeyboard> {
List<List<dynamic>> keys = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['00', '0', Icon(Icons.keyboard_backspace, size: 30,)],
];
String amount = '';
@override
void initState(){
super.initState();
// keys = [
// ['1', '2', '3'],
// ['4', '5', '6'],
// ['7', '8', '9'],
// ['00', '0', Icon(Icons.keyboard_backspace)],
// ];
// amount = ''; //textfield 를 사용하는 경우에는 이걸 쓰면 된다.
}
onKeyTap(val){
if(val == "0" && amount.length == 0){
return;
}
if(val == "00" && amount.length == 0){
return;
}
setState(() {
amount = amount + val;
});
}
onBackspacePress(){
if(amount.length == 0){
return;
}
setState(() {
amount = amount.substring(0, amount.length - 1);
});
}
renderKeyboard() {
return keys
.map(
(x) => Row(
children: x.map(
(y) {
return KeyboardKey(
label: y,
value: y,
onTap: (val) {
if(val is Widget){
onBackspacePress();
}else{
onKeyTap(val);
}
}
);
},
).toList(),
),
)
.toList();
}
renderAmount() {
String display = "얼마나 보낼까요?";
TextStyle textStyle = TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.bold,
color: Colors.grey,
);
if(this.amount.length > 0){
NumberFormat f = NumberFormat("#,###");
display = "${f.format(int.parse(amount))}매듭";
textStyle = textStyle.copyWith(
color: Colors.black,
);
}
return Expanded(
child: Center(
child : Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
display,
style: textStyle,
),
Text(
"잔액 : 10,000매듭" //api 요청
)
]
),
)
);
}
renderConfirmButton() {
//버튼
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: amount.length > 0 ? (){
} : null,
style: ElevatedButton.styleFrom(
disabledBackgroundColor: Colors.grey[400],
disabledForegroundColor: Colors.grey,
foregroundColor : Colors.black,
backgroundColor: Colors.orange,
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
"확인",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(children: [
renderAmount(),
...renderKeyboard(),
SizedBox(height: 20,),
renderConfirmButton(),
]),
),
),
);
}
}
여기도 마찬가지로 매우 길기 때문에 약간 기능을 쪼개서 보도록 하겠다.
List<List<dynamic>> keys = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['00', '0', Icon(Icons.keyboard_backspace, size: 30,)],
];
String amount = '';
@override
void initState(){
super.initState();
// keys = [
// ['1', '2', '3'],
// ['4', '5', '6'],
// ['7', '8', '9'],
// ['00', '0', Icon(Icons.keyboard_backspace)],
// ];
// amount = ''; //textfield 를 사용하는 경우에는 이걸 쓰면 된다.
}
이 부분은 키보드의 형태를 구성하고, 초기화 시키는 부분이다.
사실 봤던 강의에서는 주석 처리 되어있는 것 처럼 initState안에서 한 번 초기화를 해줬는데, nullable 문제가 생겨서..(flutter에 ?등이 도입된지 그닥 오래 지나지 않았다) 초기화를 밖에서 하는 방향으로 고친 것이다.
지금은 딱히 문제가 없긴한데, 언제 문제가 생길지 감이 안잡혀서 일단 주석으로 지워뒀다.
onKeyTap(val){
if(val == "0" && amount.length == 0){
return;
}
if(val == "00" && amount.length == 0){
return;
}
setState(() {
amount = amount + val;
});
}
이 코드는 onKeyTap 함수를 정의하고 있다. 이 함수는 키보드에서 키를 탭할 때 호출되며, 전달된 값 val에 따라 특정 작업을 수행한다.
val 값이 0인 상태에서 00과 0을 입력할 경우, 입력값이 나오면 안되기 때문에 return을 통해 눌러도 아무런 작업 없이 지나갈 수 있도록 구성했다.
setState : 상태를 업데이트 하는 것이다. 화면이 다시 그려지며, 이를 통해 값들이 더해진다.
onBackspacePress(){
if(amount.length == 0){
return;
}
setState(() {
amount = amount.substring(0, amount.length - 1);
});
}
이 부분은 앞에서 키를 눌렀을때 나오는 효과를 정의한것과 비슷하게, backpress를 했을때 나타나는 것을 정의한 것이다.
마찬가지로 길이가 0(입력값이 없음)이면 아무것도 하지 않고, 아니면 amount.substring(0, amount.length - 1)를 실행해서 문자열의 첫번째 문자부터 마지막 문자 하나 전까지의 부분 문자열을 반환한다. 즉, 마지막 문자를 제외한 나머지 문자열을 얻게 된다.
이렇게 새로 정의한 amount값을 다시 저장해서, 화면을 그린다.
renderKeyboard() {
return keys
.map(
(x) => Row(
children: x.map(
(y) {
return KeyboardKey(
label: y,
value: y,
onTap: (val) {
if(val is Widget){
onBackspacePress();
}else{
onKeyTap(val);
}
}
);
},
).toList(),
),
)
.toList();
}
renderKeyboard() 함수는 keys 리스트를 기반으로 키보드를 생성하는 역할을 한다. 이 함수는 keys 리스트의 각 요소를 반복하면서 각 행(row)의 키를 생성하고 이를 행의 리스트로 묶어 전체 키보드를 반환하는 역할을 한다.
renderAmount() {
String display = "얼마나 보낼까요?";
TextStyle textStyle = TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.bold,
color: Colors.grey,
);
if(this.amount.length > 0){
NumberFormat f = NumberFormat("#,###");
display = "${f.format(int.parse(amount))}매듭";
textStyle = textStyle.copyWith(
color: Colors.black,
);
}
return Expanded(
child: Center(
child : Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
display,
style: textStyle,
),
Text(
"잔액 : 10,000매듭" //api 요청
)
]
),
)
);
}
renderAmount() 함수는 화면에 보여줄 송금 액수를 표시하는 역할을 한다. 함수 내에서는 다음과 같은 작업을 수행한다.
renderConfirmButton() {
//버튼
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: amount.length > 0 ? (){
} : null,
style: ElevatedButton.styleFrom(
disabledBackgroundColor: Colors.grey[400],
disabledForegroundColor: Colors.grey,
foregroundColor : Colors.black,
backgroundColor: Colors.orange,
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
"확인",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
이것은 하단의 "확인"버튼을 나타내기 위한 것이다.
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(children: [
renderAmount(),
...renderKeyboard(),
SizedBox(height: 20,),
renderConfirmButton(),
]),
),
),
);
}
build 메서드는 화면을 생성하는 역할을 한다. 여기서는 Scaffold 위젯을 사용하여 기본적인 앱 레이아웃을 생성하고, 그 안에 다양한 위젯들을 배치한다.
이제 키보드의 구성요소에 대한 이해가 완료 되었으니, 내가 원하는 방향으로 바꿔볼 것이다.
원하는 화면은 다음과 같다.
닉네임과 설명을 추가해볼 것이다.
그 전에, 만든 키보드를 스캔을 구현해둔 파일로 옮겨보자!
return Expanded(
child: Center(
child : Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
// 여기에 프로필 이미지 설정
radius: 50, // 이미지 크기 설정
backgroundImage: NetworkImage(avatar), // 네트워크 이미지 사용 예시
),
SizedBox(height: 30,
),
Text(
printNickname,
style: nameTextStyle,
),
Text(
"얼마 만큼의 매듭을 보낼까요?",
style: TextStyle(
fontSize: 25.0,
color: Colors.orange,
),
),
SizedBox(height: 30,
),
Text(
display,
style: textStyle,
),
Text(
"잔액 : 10,000매듭" //api 값 가져오기
)
]
),
)
);
우선 api를 통해 받아온 프로필 사진과 닉네임이 표시되도록 하였다.(현재는 test api에서 받아왔기 때문에 닉네임이 아니라 first name이지만...)
그리고 정렬울 왼쪽 정렬로 변경하였다
가진 잔액을 같이 받아와서, 잔액 이상 입력이 되면 입력을 못하게끔 하는 기능도 구현을 해야 하는데, api 데이터를 받아 오는 방법 자체는 아니까 데이터가 마련되면 기능을 추가하려고 한다.