본문 바로가기

기타/안드로이드

Tip Time App - Switch, @StringRes, KeyboardOptions

Tip Time App - Switch, @StringRes, KeyboardOptions

맞춤 팁 계산하기  |  Android Developers

 

맞춤 팁 계산하기  |  Android Developers

작업 버튼을 추가하고 키보드 작업을 설정하고 스위치 컴포저블을 사용하는 방법을 알아봅니다.

developer.android.com

위 페이지를 참고하여 Switch, @StringRes, KeyboardOptions와 같은 개념들을 이용하여

저번에 만들었던 Tip Time App을 더 발전시켜보자

 

개요

Tip Time App, Tip 계산기에 tip 비율을 조정하고 반올림 여부를 결정할 수 있는 기능을 추가해보자

 

 

코드작성

완성된 코드를 보면서 하나하나 짚어보겠다

package com.example.tiptime

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.tiptime.ui.theme.TipTimeTheme
import java.io.StringReader
import java.text.NumberFormat

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TipTimeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    TipTimeScreen()
                }
            }
        }
    }
}

@Composable
fun TipTimeScreen() {
    var amountInput by remember { mutableStateOf("") }
    var tipInput by remember { mutableStateOf("") }
    var roundUp by remember { mutableStateOf(false) }

    val amount = amountInput.toDoubleOrNull() ?: 0.0
    val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
    val tip = calculateTip(amount, tipPercent, roundUp)

    val focusManager = LocalFocusManager.current

    Column(
        modifier = Modifier.padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ){
        Text(
            text = stringResource(R.string.calculate_tip),
            fontSize = 24.sp,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
        Spacer(Modifier.height(16.dp))
        EditNumberField(
            label = R.string.cost_of_service,
            keyboardOptions = KeyboardOptions.Default.copy(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Next
            ),
            keyboardActions = KeyboardActions(
                onNext = { focusManager.moveFocus(FocusDirection.Down) }
            ),
            value = amountInput,
            onValueChanged = { amountInput = it }
        )
        EditNumberField(
            label = R.string.how_was_the_service,
            keyboardOptions = KeyboardOptions.Default.copy(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            keyboardActions = KeyboardActions(
                onNext = { focusManager.clearFocus() }
            ),
            value = tipInput,
            onValueChanged = { tipInput = it }
        )
        RoundTheTipRow(roundUp = roundUp, onRoundUpChange = { roundUp = it })
        Spacer(Modifier.height(24.dp))
        Text(
            text = stringResource(R.string.tip_amount, tip),
            modifier = Modifier.align(Alignment.CenterHorizontally),
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold
        )

    }
}

@Composable
fun EditNumberField(
    @StringRes label: Int,
    keyboardOptions: KeyboardOptions,
    keyboardActions: KeyboardActions,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
){
    TextField(
        value = value,
        onValueChange = onValueChanged,
        label = { Text(stringResource(R.string.cost_of_service)) },
        modifier = modifier.fillMaxWidth(),
        singleLine = true,
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions
    )
}

@Composable
fun RoundTheTipRow(
    roundUp : Boolean,
    onRoundUpChange: (Boolean)->Unit,
    modifier: Modifier = Modifier
){
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .size(48.dp),
        verticalAlignment = Alignment.CenterVertically
    ){
        Text(text= stringResource(R.string.round_up_tip))
        Switch(
            modifier = modifier
                .fillMaxWidth()
                .size(48.dp),
            checked = roundUp,
            onCheckedChange = onRoundUpChange,
            colors = SwitchDefaults.colors(
                uncheckedThumbColor = Color.DarkGray
            )
        )
    }
}

private fun calculateTip(
    amount: Double,
    tipPercent: Double,
    roundUp: Boolean
): String {
    var tip = tipPercent / 100*amount
    if(roundUp)
        tip = kotlin.math.round(tip)
    return NumberFormat.getCurrencyInstance().format(tip)
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    TipTimeTheme {
    }
}

크게

1. EditNumberField 재활용해서 팁 비율 입력받기

2. Switch composable 이용해서 반올림여부 설정하기

로 나눌 수 있다

 

 

 

1. EditNumberField 재활용해서 팁 비율 입력받기

기존의 EditNumberField는

value와 onValueChange만 인자로 넣을 수 있었는데

이제는 label에 들어갈 문자열, modifier, keyboardOptions/Actions가 더 들어간다

 

코드 구조 자체는 그냥 재활용하기 위해서 인자를 더 받게 되었다는 것 밖에 볼 게 없고

문법을 좀 보도록 하자

 

@StringRes

@StringRes는 뭔가 @Composable과 같은 종류인 거 같은데 하는 일은 모르겠는 그런 친구다

이 골뱅이로 시작하는 애들은 annotation이라고 부르는데 의미가 있는 주석같은 느낌이다

 

@Composable은 밑에 있는 함수가 Composable 함수라는 의미를 가지 듯

@StringRes는 그 다음에 오는 친구가 string resource 참조여야 한다는 의미를 가진다

만약 string resource가 아니면 코드 검사과정에서 경고를 출력한다

 

 

KeyboardActions & KeyboardOptions

그리고 저번에 그냥 공식문서만 띡 링크 걸고 끝냈던 KeyboardOptions과

딱봐도 세트상품인 KeyboardActions이 나왔다

사실 여기서 쓰인 것들 다 공식문서에 잘 설명되어있지만 한번 풀어서 설명해보겠다 

KeyboardOptions(
    capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
    autoCorrect: Boolean = true,
    keyboardType: KeyboardType = KeyboardType.Text,
    imeAction: ImeAction = ImeAction.Default
)

이렇게 생긴 클래스 친구인데 잘 보면 모든 인자에 기본값이 있다

KeyboardOptions.Default는 인스턴스 없이도 접근가능한 클래스 자체 변수다

(자바의 static 변수, kotlin에서는 companion이라는 키워드로 선언한다고 한다)

Default는 기본인자로 만들어진 KeyboardOptions 객체를 값으로 가진다

 

copy는 내가 명시한 인자만 업데이트된 새로운 KeyboardOptions 객체를 만드는 메소드다

fun copy(
    capitalization: KeyboardCapitalization = this.capitalization,
    autoCorrect: Boolean = this.autoCorrect,
    keyboardType: KeyboardType = this.keyboardType,
    imeAction: ImeAction = this.imeAction
): KeyboardOptions

 

ImeAction에서 IME는 input method editor의 약자로 자판보다 많은 글자(ex 한글)의 입력을 지원하는 프로그램이라고 한다

쉽게 말해 천지인 키보드라는 거다 더 넓은 의미로는 그냥 모바일 키보드 자판이다

여기서 말하는 Action은

이런 파란버튼을 말한다

'다음 입력화면으로 이동'이나 '입력완료'같은 의미와 기능을 전달하는 역할을 한다

위 코드에서 Next와 Done이 쓰였는데,

Next는 '다음 input으로 이동'이라는 뜻이고 Done은 '입력완료'라는 의미이다

이 밖에도 search, send 등 이름만 들어도 모양도 알 것 같은 친구들이 있다

여기 공식 문서에서의 설명도 참고해보자

 

KeyboardActions은 imeAction에서 정의한 행동이 발생했을 때 일어나는 일을 결정할 수 있게 해준다

imeAction이 Next면 onNext, search면 onSearch 이런 식이다

 

FocusManager

focusManager에서 focus는 우리가 지금 입력을 하고 있는, 주시하고 있는, focus하고 있는

composable이 있을 때의 그 focus다

focusManager는 이 focus를 누구한테 할 지 어디로 옮겨갈지를 관리할 수 있게해준다

그러면 LocalFocusManager의 정체는 뭐냐? 하면 당시에는 나도 모른다고 넘어갔지만 이제는 대충 알고 있다

바로 compositionlocal 중 하나이다

이 블로그의 36. Cupcake App 글을 참고하면 다음과 같은 내용이 있다

안드로이드에는 UI tree라는 게 있는데
간단하게 설명하면 화면 상의 composable들을 노드로 한 tree이다
근데 보통 이 트리에서 우리는 더 깊은 composable에게 데이터를 넘겨줄 때 인자로 일일이 명시해서 넘겨주는데
theme 설정할 때 썼던 color와 같은 데이터는 모든 컴포저블에게 넘겨주기 매우 번거롭고 힘들어진다
그런 애들을 모아놓고 모든 UI tree에서 접근할 수 있게 해준 친구들이 있는데 그 친구들이 바로
compositionLocal이라는 친구들이다

tree 구조에서 더 밑에 있는 composable은 인자를 명시해서 데이터를 넘겨줘야 하는데

밑에 있는 다수의 composable에게 다 명시해서 넘겨주기는 귀찮으니까 전역적인? 그런 노드 역할을 해주는 것들이

compositionLocal이라는 친구들이다

 

그 중에 하나가 LocalFocusManager로, focusManager를 composable 함수의 인자로 매번 넣기 귀찮으니까

어떤 composable에서도 편히 이용하쇼!라는 느낌으로 만들어놔서

그냥 localFocusManager.current만 입력하면 접근할 수 있는 것이다

 

아무튼 focusManager를 받아오면 여기 메소드가 2개 있는데

이름만 들어도 뭐하는 애인지 알 것 같은 moveFocus와 clearFocus가 있다

 

위 코드에선 서비스의 가격을 입력하는 TextField에서는 다음 tip 비율을 입력하는 TextField로 이동할 수 있게

moveFocus(FocusDirection.Down)을 쓰고 있고

tip 비율을 입력하는 TextField에서는 clearFocus()를 쓰고 있다

 

 

 

 

 

2. Switch composable 이용해서 반올림여부 설정하기

Switch composable은 1. thumb과 2. track이라는 구조로 이루어져 있다

그... 와이파이, 블루투스 끄고 켤 때 봤던 것 같은 그 친구들이 맞다

 

Switch 문서를 보면 알겠지만

checked는 체크가 됐는지 true, false로 나타내는 property고

onCheckedChange는 체크가 되면 뭘 할 지 결정하는 '콜백'함수이다

(javascript에서 많이 들어본 Promise 어쩌구 그 친구 또는 이벤트 리스너)

SwitchDefaults는 그냥 기본 색 값 정보를 가진 객체이다

color 메소드는 위에서 본 KeyboardOptions.copy 메소드와 유사하게

내가 지정한 속성에 대해서만 색을 바꿔주는 역할을 한다

 

Switch에 대해 설명할 건 이게 다인 듯 하다

 

 

 

 

 

그 밖에 짚어볼 것

마지막으로 저번에 봤던 state hoisting을

코드에 있는 2개의 TextField와 Switch에서 찾아볼 수 있다

쉽게 말하면 TipTimeScreen()에 있는 3개의 remember 변수?가 바로 그 state들이다

 

또 string resource의 이름을 string.xml 파일에서만 바꿔도 MainActivity.kt에서도 바뀌게 할 수 있는 방법을 소개하겠다

string.xml에서 name 속성의 값을 선택하고 우클릭->Refactor->Rename을 누르면

 

이렇게 이름을 변경할 수 있고

 

우리의 피와 땀이 들어간 MainActivity.kt 파일에서 이름이 변경된 것을 확인할 수 있다

 

 

 

 

 

 

결과화면

'기타 > 안드로이드' 카테고리의 다른 글

20. My Art Gallery App  (0) 2022.10.14
Tip Time App - local & instrumentation test  (2) 2022.10.14
Tip Time App - remember API, Composable LifeCycle  (2) 2022.10.14
Lemonade App - clickable  (0) 2022.10.14
Android Studio Debugger  (0) 2022.10.14