본문 바로가기

기타/안드로이드

Tip Time App - remember API, Composable LifeCycle

Tip Time App - remember API, Composable LifeCycle

Compose의 상태 소개  |  Android Developers

 

Compose의 상태 소개  |  Android Developers

상태에 관해 알아보고 Jetpack Compose에서 상태를 사용하고 조작하는 방법을 알아봅니다.

developer.android.com

위의 페이지를 참고하여 예전에 접해봤던 remember API를 Composable LifeCycle과 함께 알아보면서

더 깊은 이해를 해보자


개요

Composable의 생명주기를 이해하면 remember API를 왜 쓰는지 조금 더 잘 알 수 있다

 

 

Composable vs Composition vs Compose

일단 컴포저블과 컴포지션과 컴포즈의 차이를 좀 구분하고 가보자

The Composition is a description of the UI built by Compose when it executes composables.
A Composition describes the UI of your app and is produced by running composables.
A Composition is a tree-structure of the composables that describe your UI.

이런 식의 설명이 있다

정리하자면

Composable은 함수이고

Composition은 Composable을 실행해서 UI tree를 만들어내는 행위이고

Compose는 라이브러리 이름이다

 

 

 

Composable의 생명주기와 remember API

저번에 remember API를 처음 배울 때 언급했던 Composable의 생명주기를 기억하는가?

1. enter the composition

2. composition(possibly recomposition)

3. leave the composition

 

이렇게 3단계로 이루어진다

그러니까 쉽게 말해서

Composable을 실행하면

1. UI를 그리러 들어가서

2. 그리고(initial composition) 반복해서 그리기도 하다가(recomposition)

3. 그리는 걸 끝낸다는 거다

 

근데 문제는 recomposition을 할 때 값이 바뀐 애를 다시 바꿔줘야 하는데

그 값이 따로 저장되는 곳이 없다

 

그래서 추적할 값들을 따로 지정하기 위해

remember API를 사용하는 것이다

 

여기서 추적할 값을 State와 MutableState 두가지 유형으로 구분할 수 있는데

State는 값을 기억하긴 하는데 바꿀 수 없는 애고

MutableState는 이름처럼 값을 바꿀 수 있는 애다

 

개념을 얼추 이해했다면

var amountInput by remember { mutableStateOf("") }

이런 코드에서

mutableStateOf가 인자인 문자열을 값으로 가지는 MutableState로 바꿔준다는 건 알겠고

remember가 저거 받아서 대충 기억하게 만드는 것까진 이해할 수 있을 것이다

 

그럼 구체적으로 코드상에서 무슨 일이 일어나는 걸까?

mutableStateOf는 빈문자열을 값으로 가지는 mutableState를 만들어서 반환한다

remember는 mutableState를 기억하고 mutableState를 반환한다

그러면 결국 amountInput by mutableState가 되는 건데

이게 무슨 의미인지는 저번에 property delegation에서 본 것과 같다

mutableState는 setValue에 대해 property delegation을 제공한다

따라서 앞으로 amountInput에 어떤 값을 할당할 때마다 mutableState의 setValue 로직을 통해 할당하게 되는 것이다

 

 

Tip 계산기 작성하기

지식은 여기까지 쌓도록 하고 이제

결과코드를 보자

package com.example.tiptime

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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("") }

    val amount = amountInput.toDoubleOrNull() ?: 0.0
    val tip = calculateTip(amount)

    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(value = amountInput, onValueChanged = { amountInput = 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(
    value: String,
    onValueChanged: (String) -> Unit
){
    TextField(
        value = value,
        onValueChange = onValueChanged,
        label = { Text(stringResource(R.string.cost_of_service)) },
        modifier = Modifier.fillMaxWidth(),
        singleLine = true,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
    )
}

private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0
): String {
    val tip = tipPercent / 100*amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

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

코드에서 짚고 넘어갈게

1. toDoubleOrNull과 elvis 연산자

2. Arrangement.spaceBy

3. align(Alignment.CenterHorizontally)

4. TextField

5. NumberFormat.getCurrencyInstance().format(tip)

이렇게 5개가 있다

 

1. toDoubleOrNull과 elvis 연산자

toDoubleOrNull은 String을 숫자로 변환하거나 숫자가 없으면 null로 변환해주는 메소드다

elvis연산자는 오랜만에 봤지만 한번 배웠던 건데, 만약 값이 null이면 어떤 값을 할당할지 지정할 수 있게 해주는 연산자다

 

2. Arrangement.spaceBy

예전에 UI 구성을 배우면서 나왔었던 Arrangement의 여러 값 중 하나로,

composable 사이의 간격을 정해진 값만큼으로 띄워준다

 

3. align(Alignment.CenterHorizontally)

마찬가지로 UI 구성을 배우면서 나왔었던 내용인데,

textAlign = Alignment.CenterHorizontally와 같은 뜻이다

 

4. TextField

TextField는 문자를 입력받는 Composable이다

그 뭐냐 html로 치면 input type="text" 같은 느낌 텍스트박스 정도

근데 들어가는 속성이 어마무시하게 많으니까 좀 찬찬히 살펴보자

 

value는 이 TextField가 표시하는 값을 의미한다

처음에는 아무 값도 없겠지만 우리가 입력을 하면 value가 바뀌고 표시되는 값도 바뀌는 것이 일반적이다

 

onValueChange는 onClick처럼 이벤트트리거? 콜백함수? 같은 친구이다

문자를 입력받는 것이 바로 ValueChange이니까 문자를 입력받으면 무슨 행동을 할 지 결정해주면 된다

lambda expression을 배울 때 짚었던 것 처럼 저 it은 명시가 생략된 매개변수이다

그러니까 매개변수 값으로 들어오는 String 값을 amountInput에 할당하겠다는 것이다

 

label은 위 그림의 Cost of Service와 같이 TextField의 제목이다

 

singleLine은 html에서 input type="text"로 할거냐 input type="textarea"로 할거냐의 차이다

이름만 봐도 알 수 있듯이 한줄만 입력받을 것인지 여러줄을 입력받을 것인지로 보면 된다

 

keyboardOptions는 그냥 봐도 알 것 같으니까 저거 말고 무슨 속성값이 더 가능한가 구경해보자

크게 대문자/소문자, 키보드타입, 자동맞춤 등을 설정할 수 있는 거 같다

 

5. NumberFormat.getCurrencyInstance().format(tip)

얘는 Java에서 온 친구라고 한다

 

Java에 Number라는 자료형이 있는데 그 Number의 포맷을 지정해주는 친구가 NumberFormat이라고 한다

엑셀에서 숫자를 소숫점으로 표시할지 돈으로 표시할지 선택하는 표시형식 같은 느낌이다

 

근데 이 친구는 팩토리 패턴(tmi : 예전에 디자인패턴이 뭔지 공부해볼 때 봤었지만 기억이 나지 않음)으로

만들어져서 인스턴스를 만들지 못하고 인스턴스 반환 메소드를 사용해야한다고 한다

여기서 쓰인 getCurrencyInstance()가 바로 돈과 같은 표시형식을 가진 Number 자료형의 인스턴스를 반환하는 친구이다

 

그 다음 format이라는 친구는 그 자바에서 %d %s할 때 쓰는 String.format()이 아니라

NumberFormat의 메소드이다

(아니 생각해보니 당연한거네 인스턴스를 반환해놓고 체인을 붙였으니까 ㅋㅋ

tmi : 이게 뭔 함수인지 찾는데 좀 걸렸음)

 

암튼 저기에 Double형 변수 tip을 넣으면 화폐단위이면서 문자열인 녀석으로 변환되서 나온다고 한다

 

 

 

state hoisting

짚어볼만한 문법적인 포인트들을 다 짚어봤으니 마지막으로 개념하나 더 보고 가도록 하자

바로 state hoisting이다

hoisting은 대충 끄집어내는, 끌어올리는 그런 뜻의 단어인데

상태를 끄집어내는 게 무슨 소리냐하면

 

Composable의 안에 추적중인 상태가 있다고 해보자(remember 써서 변수 하나 만들었다는 의미)

그러면 다른 Composable은 그 상태를 볼 수가 없고

만약 그 상태에 맞춰 변화해야 한다고 해도 변할 수가 없다

볼 수가 없으니까

 

그래서 Composable 안에 있는 추적중인 상태를 Composable 밖으로 끄집어내서

다른 애들도 볼 수 있게 하는 것이 state hoisting이다

 

우리가 만든 Tip 계산기 예제도 state hoisting이 적용됐다

amountInput을 TextField Composable안에 둔 것이 아니라 밖에 있는 TipTimeScreen Composable에 뒀으니까

덕분에 우린 이제 저 amountInput을 여러 Composable에서 참고하게 만들 수 있게 됐다

 

 

결과화면

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

Tip Time App - local & instrumentation test  (2) 2022.10.14
Tip Time App - Switch, @StringRes, KeyboardOptions  (0) 2022.10.14
Lemonade App - clickable  (0) 2022.10.14
Android Studio Debugger  (0) 2022.10.14
Dice Roller App - remember API  (0) 2022.10.14