티스토리 뷰

 

상태

모든 안드로이드 앱에서는 사용자에게 상태가 표시된다 안드로이드 앱 상태의 몇가지 예시는 다음과 같다

1. 네트워크 연결을 설정할수 없을때 표시되는 메시지

2. 상태를 작성하고 제출할수있는 양식

3. 버튼과 같이 탭할 수 있는 것들이 지금 탭 안된상태인지, 탭하는 중인지, 탭이 끝났는지

등등..

 

Compose 의 상태

  • Compose는 선언형 UI프레임워크로 UI의 모습을 코드로 선언하는것이다.
  • 컴포지션: 컴포즈가 컴포저블을 실행할때 빌드한 UI (?)에 대한것? 몬말이지
  • 리컴포지션: 상태가 변경되면 컴포즈는 상태 변경에 영향을 받는(모습이 변해야하는?) 컴포저블 함수를 새 상태로 다시 실행한다, 그러면 리컴포지션이라는 업데이트된 UI가 만들어진다. 컴포즈는 자동으로 리컴포지션을 예약한다.
  • 초기 컴포지션시 처음으로 컴포저블을 실행할때 컴포지션에서 UI를 기술하기 위해 호출하는 컴포저블을 추적한다
    • 맨 처음 컴포저블 함수 실행할때 그 안에 엮인? 컴포저블을 모두 추적한다는 뜻인듯?
  • 리컴포지션은 데이터 변경사항에 따라 변경될 수 있는 컴포저블을 다시 실행한 다음 변경사항을 반영하도록 컴포지션을 업데이트 하는 것
  • 컴포지션은 초기 컴포지션을 통해서만 생성되고 리컴포지션을 통해서만 업데이트된다
  • 리컴포지션을 하기 위해서는 컴포즈가 추적할 상태를 알아야 한다(변경되면 ui업데이트 해야되는 변수를 추적, 관찰 해야된다는뜻?인듯)
  • 컴포즈에선 이 추적, 관찰 해야되는 상태를 만들때 State, MutableState를 사용해서 만든다
    • State: 변경 불가, 그 유형의 값을 읽기만 할수있다
    • MutableState: 변경가능, 읽고 쓸수있다
      • mutableStateOf() 함수를 사용해 MutableState를 만든다
      • mutableStateOf() 함수에서 반환하는 값은 다음과 같은 특성이 있다
        • 상태를 보유한다
        • 변경 가능하다
        • 관찰가능하므로 컴포즈는 mutable state의 값 변경을 관찰하고 얘가 변경되면 리컴포지션을 트리거해 UI업데이트를 한다
  • remember
    • remember로 계산된 값은 초기 컴포지션 중에 컴포지션에 저장된다(대체 컴포지션이 몰까)
    • 저장된 값은 리컴포지션 중에 반환된다
    • 일반적으로 remember와 mutableStateOf가 함께 쓰인다

 

  • 스테이트리스 컴포저블: state, mutable state 즉 상태가 없는 컴포저블을 의미함
    • 스테이트풀과 스테이트리스 컴포저블 비교
      • 스테이트리스: 상태 없음, 새 상태를 보관하거나 정의하거나 수정하지 않음
      • 스테이트풀: 시간이 지남에 따라 변할 수 있는 상태를 소유하는 컴포저블

 

  • 상태 호이스팅
    • stateful 한 컴포저블을 stateless하도록 만들기 위한 디자인 패턴
    • 원래 하위 컴포저블에 있던 상태를 옮겨서 상위 컴포저블에 상태를 정의하고 하위 컴포저블은 stateless하게 만든다
    • 다음과 같은 경우 상태를 호이스팅 해야함
      • 상태를 여러 컴포저블과 공유하는 경우
      • 앱에서 재사용 가능한 stateless 컴포저블을 만드는 경우
    • 실제 앱에서는 컴포저블 기능에 따라 100% 스테이트리스 하게 만드는것은 어려울수있다
    • 가능한 컴포저블이 상태를 적게 소유하게 해야하며 컴포저블의 API에 상태를 노출해 상태 호이스팅이 되도록 컴포저블을 디자인해야함
    • 이 패턴이 컴포저블에 적용되는 경우 컴포저블 매개변수에 다음 2개가 추가될 때가 많다
      • value: T 매개변수: 표시할 현재 값
      • onValueChange: (T) -> Unit :사용자가 텍스트 상자에 텍스트를 입력하는 경우 등 값이 변경될때 상태가 업데이트 되도록 트리거되는 콜백 람다
package com.example.tiptime

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
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.painterResource
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.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(){
    Column(
        modifier = Modifier.padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(
            text = stringResource(id = R.string.calculate_tip),
            fontSize = 24.sp,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
        Spacer(modifier = Modifier.height(16.dp))
        EditNumberField()
        Spacer(modifier = Modifier.height(24.dp))
        Text(//이곳에 EditNumberField의 tip이라는 변수값이 적혀져야 하는데 그럴수없다 -> 상태 호이스팅 한다
            text = stringResource(R.string.tip_amount, ""),
            modifier = Modifier.align(Alignment.CenterHorizontally),
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold
        )
    }

}
@Composable
fun EditNumberField(){
    var amountInput by remember { mutableStateOf("") }//입력창에 표시될 값
    val amount=amountInput.toDoubleOrNull() ?: 0.0//amountInput의 값을 Double혹은 null형으로 변환
    val tip= calculateTip(amount)//amount값을 calculateTip에 넣어 내야할 팁값을 계산한다
    TextField(
        value = amountInput,//입력창에 표시될 값
        onValueChange ={//입력된 값이 it에 들어온다
            amountInput=it//입력된 값을 amountInput에 넣어 입력창에 표시되게 함
        },//사용자가 입력창에 텍스트를 입력할때 일어날 일을 적으면 된다
        label={ Text(stringResource(id = R.string.cost_of_service))},//라벨에 표시될것
        //label = { Image(painter = painterResource(id = R.drawable.ic_launcher_background), contentDescription = "hi")},
        modifier = Modifier.fillMaxWidth(),
        singleLine = true,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
    )
}

private fun calculateTip(
    amount:Double,//낸 돈
    tipPercent: Double=15.0//기본 팁값 15%
) : String {//이 함수의 반환은 String
    val tip = tipPercent / 100 * amount //팁값
    return NumberFormat.getCurrencyInstance().format(tip)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    TipTimeTheme {
        TipTimeScreen()
    }
}

위 코드는 호이스팅 전의 모습

위 코드의 구조를 그림으로 나타내면 이렇다

 

팁 상태값에 엑세스 불가하므로 상태 호이스팅 구조를 적용한다

상태 호이스팅을 적용한 코드는 아래와 같다

package com.example.tiptime

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
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.painterResource
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.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(){
    /*호이스팅된 변수들 3개 amountInput, amount, tip*/
    var amountInput by remember { mutableStateOf("") }//입력창에 표시될 값
    val amount=amountInput.toDoubleOrNull() ?: 0.0//amountInput의 값을 Double혹은 null형으로 변환
    val tip= calculateTip(amount)//amount값을 calculateTip에 넣어 내야할 팁값을 계산한다

    Column(
        modifier = Modifier.padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(
            text = stringResource(id = R.string.calculate_tip),
            fontSize = 24.sp,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
        Spacer(modifier = Modifier.height(16.dp))
        EditNumberField(
            value=amountInput,
            onValueChange = { amountInput=it }
        )
        Spacer(modifier = Modifier.height(24.dp))
        Text(//이곳에 EditNumberField의 tip이라는 변수값이 적혀져야 하는데 그럴수없다 -> 상태 호이스팅 한다
            text = stringResource(R.string.tip_amount, tip),
            modifier = Modifier.align(Alignment.CenterHorizontally),
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold
        )
    }

}
@Composable
fun EditNumberField(
    value: String, //호이스팅 된 값을 받아옴
    onValueChange: (String)->Unit //호이스팅 된 값 업데이트하는 콜백함수수
){    TextField(
        value = value,//입력창에 표시될 값
        onValueChange =onValueChange,
        label={ Text(stringResource(id = R.string.cost_of_service))},//라벨에 표시될것
        modifier = Modifier.fillMaxWidth(),
        singleLine = true,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
    )
}

private fun calculateTip(
    amount:Double,//낸 돈
    tipPercent: Double=15.0//기본 팁값 15%
) : String {//이 함수의 반환은 String
    val tip = tipPercent / 100 * amount //팁값
    return NumberFormat.getCurrencyInstance().format(tip)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    TipTimeTheme {
        TipTimeScreen()
    }
}

구조는 아래 그림과 같다

 


Tip Time 앱 만들기

-퍼센트를 입력하면 팁을 얼마 줘야되는지 표시하는 앱이다.

 

  • Arrangement.spaceBy()
    • 이 컴포저블 하위 요소들 사이에 8dp 만큼의 공백이 추가된다
Column(
        modifier = Modifier.padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {

    }
  • 입력창 컴포저블
    • TextField 컴포저블을 사용한다
    • 사용자가 입력창에 텍스트를 입력하면 onValueChange에 있는 람다 콜백이 호출됨
    • 아래 코드의 amountInput은 mutableStateOf로 만들어져 변경 가능한 상태이며 리컴포지션이 예약된다
      remember를 사용하므로 변경사항은 리컴포지션이 끝나도 초기화 되지 않고 유지된다
    • 입력창에 사용자가 숫자를 입력하더라도 입력된 값은 onValueChange 콜백함수에 String으로 반환된다 반환된 값 변수 이름은 it
  •  
var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,//입력창에 표시될 값
        onValueChange ={
            amountInput=it//입력된 값이 it에 들어온다
        }//사용자가 입력창에 텍스트를 입력할때 일어날 일을 적으면 된다
    )
TextField(
        value = amountInput,//입력창에 표시될 값
        onValueChange ={
            amountInput=it//입력된 값이 it에 들어온다
        },//사용자가 입력창에 텍스트를 입력할때 일어날 일을 적으면 된다
        //label={ Text(stringResource(id = R.string.cost_of_service))}//라벨에 표시될것
        label = { Image(painter = painterResource(id = R.drawable.ic_launcher_background), contentDescription = "hi")}
        //이미지도 들어감
    )

이미지를 라벨로 했을때
이미지를 라벨로 하고 값 입력
텍스트를 라벨로 할때
텍스트를 라벨로 하고 입력

  • 입력창 single line 속성
    • singleLine을 true로 하면 텍스트 상자가 여러 줄에서 가로로 스크롤 가능한 하나의 줄로 압축됨
TextField(
  // Other parameters
   singleLine = true,
)

singleLine이 true아닐때

 

singleLine이 true일때

 

  • 입력창 KeyboardOptions 속성
    • 입력창을 눌렀을때 어떤 타입의 키보드가 나타나게할지 정한다
    • https://developer.android.com/reference/kotlin/androidx/compose/ui/text/input/KeyboardType?hl=ko&authuser=1 <-키보드 타입들은 여기참고
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)

숫자만 있는 키보드

 

  • mutableStateOf()
    • 관찰가능하고 변경가능한 mutable state를 만든다
    • by remember는 코틀린의 속성 위임
      • 아래 코드 2번줄의 value 변수 속성의 기본 getter함수와 setter함수가 remember클래스의 getter, setter함수에 각각 위임된다(아 쒸 위임 너무어렵다)
    • 아래 세가지 방법으로 mutable state를 만들수있음
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
  • NumberFormat
    • 특정 숫자값에 대한 출력을 할때 큰 값인 경우 "," 가 들어가게 숫자를 출력하고 싶을수있음
    • 1,000,000 이런식으로
    • 이럴때 NumberFormat을 씀
    • NumberFormat.getInstance(위치정보).format(int형 변수) 이런식으로 사용됨
      • getInstance에는 특정 locale정보가 들어가야함
      • Locale.getDefault()를 사용하면 현재 휴대폰 시스템의 지역 정보를 가져온다
      • Locale.US 처럼 정보를 가져와도 된다
    • 만약 표시할 숫자가 통화(돈) 을 나타낼 경우 getCurrencyInstance를 사용
      • Number.getCurrencyInstance(Locale.US).format(변수)
        • 이렇게 하면 출력되는 문자열은 $100,000.00 이런식으로 출력됨
  • toDoubleOrNull()
    • 사전 정의된 코틀린 함수로 문자열을 Double숫자로 파싱, 문자열이 숫자 변환 불가한 경우 null로 변환한다
    • null로 변환될 경우를 대비하여 엘비스 연산자 ?: 를 이용해 null이면 0.0이 되도록함
val amount = amountInput.toDoubleOrNull() ?: 0.0
  • 위치형식(문자열 복수형)
    • strings.xml 에 이런 문자열이 필요하다고 가정하자
      • "상자수: 5개"
    • 그런데 이런경우 상자의 수가 1개일수도 2개일수도 3개일수도 있다 "5"부분이 변경 가능해야한다
    • 그럴땐 아래처럼 해주면된다
//strings.xml
<string name="box_num">상자 수: %s 개</string>
//문자열 사용 코드
var box=5
Text(
    text = stringResource(R.string.box_num, box)
)

 


결과 화면

 

 

 

-참고사이트

https://developer.android.com/codelabs/basic-android-kotlin-compose-using-state?hl=ko&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-2-pathway-3%3Fhl%3Dko%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-using-state#0 

 

Compose의 상태 소개  |  Android Developers

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

developer.android.com

https://developer.android.com/jetpack/compose/state

 

상태 및 Jetpack Compose  |  Android Developers

상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이는 매우 광범위한 정의로서 R

developer.android.com

https://jootc.com/p/201906022871

 

[안드로이드/JAVA] 통화 및 숫자 형식의 포맷 나타내기 (NumberFormat) - JooTC

안드로이드 통화 및 숫자 포맷 처리 특정 숫자값에 대한 화면 출력을 위해서 다음과 같이 콤마(,)가 들어간 숫자 형식을 사용하고 싶은 경우가 있습니다. 이럴 때는 NumberFormat 클래스를 사용할 수

jootc.com

https://kotlinworld.com/244

 

[Android Compose State] State Hoisting(상태 호이스팅) 패턴이란 무엇인가?

Compose의 State 선언형 UI 프레임워크인 Compose는 Stateless함이 가장 큰 장점이다. UI에 대한 UI상태의 상호 의존성을 끊을 수 있다면 UI의 재사용성이 생기고, UI에 대한 테스트 또한 가능해지기 때문이

kotlinworld.com

https://developer.android.com/jetpack/compose/resources?hl=ko#strings 

 

Compose의 리소스  |  Jetpack Compose  |  Android Developers

Compose의 리소스 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 Android 프로젝트에 정의된 리소스에 액세스할 수 있습니다. 이 문서에서는 이를

developer.android.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함