BasicTextField 의 단점을 보완한 BaseTextField2를 프로젝트에 적용해보면서, 기존의 BasicTextField 과 달라진 점이 무엇인지, 직접 확인해보도록 하겠다.
BasicTextField2에 적용된 두드러진 변화는 다음과 같다.
BasicTextField 에선
var username by rememberSaveable { mutableStateOf("") }
BasicTextField(
value = username,
onValueChange = { username = it },
)
다음과 같이 작성해야 했던 코드를
val username = rememberTextFieldState()
BasicTextField2(state = username)
이와 같이, State를 사용하는 다른 Compose API 들과 같이, 간편하게 작성해줄 수 있게 되었다.(ex)ScrollState, LayListState)
콜백이 아닌, State 를 사용하기에 좀 더 선언형, Compose 스럽게 바뀐것 같다.
물론 TextFieldState() 뷰모델에서도 사용이 가능하며, 더 이상 입력 텍스트를 관리하기 위해 rememberSaveable 이나 savedStateHandle 등의 별도 처리를 해주지 않아도 된다.
더욱이 TextFieldState 를 flow 로 변환해주는 .textAsFlow() 와 같은 flow 타입으로 변환하는 함수를 제공하여, stateIn 연산자와 함께 사용할 경우 간편하게, StateFlow 로 만들어 줄 수 도 있다.
이를 이용하여 뷰모델 내에서 입력한 text 에 대한 validation 을 수행하거나,
Jetpack Compose 에서 TextField를 이용하여 자동 검색 기능 구현 하기
해당 글에서 구현했던 자동 검색과 같은 기능을 지원하려고 할 때, 더 간단한 코드를 통해 요구사항을 만족할 수 있다.
더욱 자세한 내용을 알고 싶다면 해당 글을 참고하면 좋습니다.
BasicTextField2: A TextField of Dreams
이와 같은 장점들을 직접 알아보기 위해, 글의 썸네일로 등장한 텍스트필드를 BasicTextField2 를 이용해서 구현해보고자 한다.
사실 그렇다.
그런데, OutlinedTextField 의 경우 Custom 하기 껄끄러운 면이 있는데, 피그마 시안과 디자인을 정확하게 !
맞추기 위해, 텍스트의 입력 시작점의 위치를 기본 위치보다 좀 더 왼쪽으로 옮긴다던지, trailingIcon의 위치를 좀 더 텍스트필드의 외각으로 위치시킨다던지 할 때, 한계가 존재한다.내 숙련도 부족일 수 있다
하지만 BasicTextField 를 사용하는 경우 decorationBox(BasicTextField2의 경우엔 decorator) 파라미터를 통해 innerTextField 의 위치를 좀 더 상세히 지정할 수 있어 Custom 에 용이하다.
위의 텍스트필드를 구현한 코드는 다음과 같은데 decorator 파라미터에 대입한 값을 확인해보면 이해할 수 있을 것 이다.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchTextField(
searchText: TextFieldState,
@StringRes searchTextHintRes: Int,
onSearch: (String) -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color = Color.White,
cornerShape: RoundedCornerShape = RoundedCornerShape(67.dp),
borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = Color(0xFFD9D9D9)),
) {
BasicTextField2(
modifier = Modifier.fillMaxWidth(),
state = searchText,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
textStyle = TextStyle(color = Color(0xFF848484)),
decorator = { innerTextField ->
Row(
modifier = modifier
.background(color = backgroundColor, shape = cornerShape)
.border(
border = borderStroke,
shape = cornerShape,
),
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.width(17.dp))
if (searchText.text.isEmpty()) {
Text(
text = stringResource(id = searchTextHintRes),
color = Color(0xFF848484),
style = BoothLocation,
)
}
innerTextField()
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
contentDescription = "Search Icon",
modifier = Modifier.clickable {
onSearch(searchText.text.toString())
},
)
Spacer(modifier = Modifier.width(width = 15.dp))
}
},
)
}
OutlineTextField 에서 제공하는 trailingIcon이나, hint 값을 제공할 수 있는 placeholder 는 지원하지 않지만, 이는 위에 코드에서 볼 수 있듯, 구현하는데 크게 어려움은 없다.
SearchTextField(
searchText = uiState.searchText,
searchTextHintRes = R.string.intro_search_text_hint,
onSearch = { query -> Timber.d("검색: $query") },
modifier = Modifier
.height(46.dp)
.fillMaxWidth()
.padding(horizontal = 20.dp),
)
위의 SearchTextField 컴포저블을 화면 내에서 사용하기 위해 위와 같이
코드를 작성하고 실행 해보았는데, 문제가 발생하였다.
텍스트필드를 터치하고 아직 입력을 하지 않았을 때, hint 텍스트의 끝단에서 커서가 깜빡거린다!...그리고 텍스트를 입력하면 정상적으로 커서가 위치하게 된다.
당연한게 Row 내에서 placeholder 역할을 수행하는 text가 먼저 위치하고, 그 다음에 innerTextField()가 위치하기 때문에, 커서 역시 text의 오른쪽 끝에 존재하는 것이다... 흠...
나는 다음과 같이
decorator = { innerTextField ->
Row(
modifier = modifier
.background(color = backgroundColor, shape = cornerShape)
.border(
border = borderStroke,
shape = cornerShape,
),
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.width(17.dp))
// Box 로 래핑
Box {
if (searchText.text.isEmpty()) {
Text(
text = stringResource(id = searchTextHintRes),
color = Color(0xFF848484),
style = BoothLocation,
)
}
innerTextField()
}
//
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
contentDescription = "Search Icon",
modifier = Modifier.clickable {
onSearch(searchText.text.toString())
},
)
Spacer(modifier = Modifier.width(width = 15.dp))
}
},
Box 로 한번 더 감싸는 방법을 사용하여, 문제를 우선 해결하였다. Box 의 특성상 내부의 컴포저블들은 위로 쌓이는 형식이기 때문에 innerTextField 의 커서의 위치도 원하는 곳에 위치 시킬 수 있다.
하지만 좀 더 근본적인 해결 방법이 있을 것 같다... 먼가 임시방편적으로 문제를 해결한 것 같은 느낌이 있어, 좀 더 좋은 해결 방법이 있는지는 고민해봐야겠다.
더 좋은 방법이 있다면 덧글 부탁드립니다ㅎ
위의 문제는 BasicTextField2 자체의 버그(이슈)나 문제는 아니지만, BasicField 1, 2 를 이용하여 Custom TextField 를 구현 할 때, 빈번하게 발생할 수 있는 문제라, 글을 작성하게 되었다.
아직 TextField2 시리즈는 BasicTextField2만 존재하고, TextField2 나 OutlineTextField2 는 존재하지 않는데, 얘네도 2가 나와줬으면 좋겠다.
BasicTextField 가 글에서 언급한 것 처럼, Custom 에 용이하긴 하지만, 위의 TextField 들에서 기본적으로 지원되는 옵션들 조차, 전부 직접 구현해줘야하기 때문에, 공수가 들어가는 것은 사실이기 때문이다.
참고)
https://www.youtube.com/watch?v=YFS2EfGJBJk
https://proandroiddev.com/basictextfield2-a-textfield-of-dreams-1-2-0103fd7cc0ec
https://reco-dy.tistory.com/16