본문 바로가기
Project/Project

[Compose] 상태에 따라 변하는 텍스트 입력창(TextField) 만들기

by 겸 2023. 10. 11.

(컴포즈를 공부하면서 기록하는 글!)

디자이너의 문서에 맞추어 로그인이나 회원가입 시 공통적으로 자주 사용할 TextField를 재사용이 편리하도록 미리 만들어보려 한다.

가장 먼저 기본적인 TextField를 Android Studio 화면에 띄워보자.

@Composable
fun FilledTextField(){
    var text by rememberSaveable { mutableStateOf("") }
    TextField(
        value = text,
        onValueChange = { text = it },
        singleLine = true
    )
}

@Preview
@Composable
fun PreviewTextField() {
    FilledTextField()
}

Composable함수를 만들기 위해서는 함수 이름에 @Composable을 추가해야 한다.

AndroidStudio 내에서 Composable함수를 미리 보기위해서는 @Composable 앞에 @Preview를 추가하면 된다. (Android Developer 문서)

위 코드를 보면 xml을 사용할 때와 다르게 rememberSaveable이 있다. 왜 이것을 사용해야 할까?

TextField의 구성

텍스트를 입력하는 부분과 텍스트를 저장하는 부분으로 이루어져 있다.

  1. 기존 XML는 Stateful했다.
    1. 즉, 텍스트필드 자체에 입력하는 부분과 저장하는 부분이 모두 존재했다.
  2. Compose는 Stateless이다.
    1. 텍스트를 입력하는 부분만 있고, 저장하는 공간이 없어 State를 따로 만들어주어야 한다.
    2. 텍스트필드의 상태가 바뀔 때마다 Recompose가 일어나므로 재구성되기 전의 상태를 저장하고, 재구성 후 다시 복구해야한다.

Remember vs RememberSaveable

문서를 보면 두가지를 Remember를 사용하는데 rememberSaveable은 화면이 회전되거나 프로세스가 종료될 때도 상태를 보존한다.


에러 상태 처리

유효하지 않은 값을 입력했을 때, 빨간색 밑줄이 생기며 에러 메시지가 표시되도록 만들어보자.

실행화면

  • 재사용을 위해 직접만든 CustomFilledTextField()
    • value, onValueChange는 필수 파라미터로 설정하고, 나머지는 optional로 기본 값을 미리 입력했다.
  • TextField의 설정
    • isError변수로 현재 상태가 에러인지 여부를 지정한다.
    • supportingText는 입력창 밑에 설명에 해당하는 메시지로 에러상태일때만 나타나도록 했다.
    • colors를 통해 배경은 모두 투명색으로 지정해주었고, 에러 발생 시 에러 메시지와 밑줄 색상이 빨간색이 되도록 설정했다.
  • PreviewTextField()
    • 에러여부를 여기서 지정해주어야한다.
    • 다음의 예시코드는 사용자가 “error”이라는 텍스트를 입력했을 때, isError변수의 값이 바뀌어 TextField가 에러상태로 변하게 된다.
    • 입력창의 상태가 바뀌면 다시 Recompose를 하므로 에러가 아닌 상태를 새로 그리게 된다.
@Composable
fun FilledTextField(
    value: String,
    onValueChange: (String) -> Unit,
    isError: Boolean = false,
    errorMessage: String = ""
) {
    TextField(
        value = value,
        onValueChange = onValueChange,
        singleLine = true,
        isError = isError,
        supportingText = { if (isError) Text(text = errorMessage) else Text(text = "") },
        colors = TextFieldDefaults.colors(
            errorSupportingTextColor = Color.RED, // 에러 메시지 
            focusedIndicatorColor = Color.GREEN, // 밑줄
            errorIndicatorColor = Color.RED,
            unfocusedContainerColor = Color.Transparent, // 배경
            disabledContainerColor = Color.Transparent,
            errorContainerColor = Color.Transparent,
            focusedContainerColor = Color.Transparent,
        )
    )
}

@Preview
@Composable
fun PreviewTextField() {
    val text = rememberSaveable { mutableStateOf("") }
    val isError = text.value == "error"

    FilledTextField(
        value = text.value,
        onValueChange = { textValue -> text.value = textValue },
        isError = isError,
        errorMessage = "error"
    )
}

Label 설정

label = { Text(text = label) }

Material3의 문서에서 좌측 이미지에는 Label text의 위치가 가운데 있고, 우측에는 상단에 있다.

이는 indicator의 상태가 enabled인지 focused인지에 따라 위치가 달라짐을 의미한다.

하지만 우리 디자이너의 요청사항은 enable상태일 때는 Label Text가 나타나지 않도록 하는 것이다.

이를 위해 라벨 텍스트 색상을 조정해주었다.

unfocusedLabelColor = Color.Transparent

Placeholder 설정

두개의 텍스트 필드 모두 placeholder를 설정해주었고,

이떄 첫번째 텍스트 필드는 라벨을 설정하고, 두번째 텍스트 필드는 라벨을 설정하지 않은 모습이다.

아래 textField는 클릭을 하면 초록색 라인과 함께 placeholder가 뜨지만 클릭하지 않는다면 아무것도 뜨지 않는다.

label과 placeholder를 모두 설정하면 클릭하지 않은 상태에서 label만 보이게 된다.

따라서 placeholder를 설정해도 클릭하지 않은 상태에서 label이 투명색으로 가리고 있기 때문에 placeholder가 보이지 않게 된다.

사용자가 라벨을 설정하지 않았을 때는 unfocused 상태에서도 placeholder가 보이게 하려면 어떻게 할까?

직접 파라미터에서 label, placeholder의 초깃값을 ""로 설정하고, label을 설정하지 않았다면 label을 null로 설정해주었다.

@Composable
fun FilledTextField(
    value: String,
    onValueChange: (String) -> Unit,
    label: String = "",
    placeholder: String = "",
    textAlign: TextAlign = TextAlign.Left,
    isError: Boolean = false,
    errorMessage: String = ""
) : Unit
label = if (label != "") {
    { Text(text = label) }
} else null

실행 결과, 아래 텍스트필드는 클릭하지 않아도 힌트(placeholder)가 뜬다~!


다양한 입력 방법들

비밀번호 입력창

비밀번호 입력 시 텍스트 숨기기 여부를 아이콘 버튼의 클릭을 통해 구현햘 수 있다.

아이콘의 변화는 색상이나 이미지의 변경으로 설정할 수있다.

var passwordHidden by rememberSaveable { mutableStateOf(true) }

// TextField 내부 설정
visualTransformation = if (passwordHidden) PasswordVisualTransformation() else VisualTransformation.None,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
    IconButton(onClick = { passwordHidden = !passwordHidden }) {
        val iconColor = if(passwordHidden) Color.Green else Color.LightGray
        val description = if (passwordHidden) "Show password" else "Hide password"
        Icon(imageVector = Icons.Filled.Visibility, contentDescription = description, tint = iconColor)
    }
}

타이머가 존재하는 입력창

제한 시간 내에 특정 입력을 해야할 때 쓰는 입력창이다. 주로 인증번호를 요청하고 입력할 때 사용된다.

처음에는 시간을 suffix를 사용해 설정했지만, suffix는 클릭을 해야 나타난다는 단점이 있었다.

이를 대체해서 trailingIcon을 활용했다.

LaunchedEffect를 사용해서 Composable 내에서 정지함수를 호출할 수 있다.

key를 기준으로 suspend fun을 취소하고 재실행할 수 있다.

var timeLeft by rememberSaveable { mutableStateOf(seconds) }
LaunchedEffect(key1 = timeLeft, key2 = isPaused) {
    while (timeLeft > 0 && !isPaused) {
        delay(1000L)
        timeLeft--
    }
}

trailingIcon에 텍스트로 남은 시간을 표시해주었다.

trailingIcon = { Text(text = "$timeLeft") }

링크

Material3의 TextField문서

Android Developer의 TextField문서

반응형