티스토리 뷰
상태
모든 안드로이드 앱에서는 사용자에게 상태가 표시된다 안드로이드 앱 상태의 몇가지 예시는 다음과 같다
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에 들어온다
}//사용자가 입력창에 텍스트를 입력할때 일어날 일을 적으면 된다
)
- 입력창에 라벨 추가
- https://m3.material.io/components/text-fields/overview#anatomy <-texg fields의 구성요소들
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,
)
- 입력창 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 이런식으로 출력됨
- Number.getCurrencyInstance(Locale.US).format(변수)
- toDoubleOrNull()
- 사전 정의된 코틀린 함수로 문자열을 Double숫자로 파싱, 문자열이 숫자 변환 불가한 경우 null로 변환한다
- null로 변환될 경우를 대비하여 엘비스 연산자 ?: 를 이용해 null이면 0.0이 되도록함
val amount = amountInput.toDoubleOrNull() ?: 0.0
- 위치형식(문자열 복수형)
- strings.xml 에 이런 문자열이 필요하다고 가정하자
- "상자수: 5개"
- 그런데 이런경우 상자의 수가 1개일수도 2개일수도 3개일수도 있다 "5"부분이 변경 가능해야한다
- 그럴땐 아래처럼 해주면된다
- strings.xml 에 이런 문자열이 필요하다고 가정하자
//strings.xml
<string name="box_num">상자 수: %s 개</string>
//문자열 사용 코드
var box=5
Text(
text = stringResource(R.string.box_num, box)
)
결과 화면
-참고사이트
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
[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
'공부 > ComposeCamp 2022' 카테고리의 다른 글
Unit 2: 자동테스트 (0) | 2022.11.30 |
---|---|
Unit 2 : 맞춤 팁 계산기 (0) | 2022.11.29 |
unit 2 : 레몬에이드 앱 만들기 (0) | 2022.11.27 |
unit2 : 안드로이드 스튜디오 디버거 사용하기 (0) | 2022.11.27 |
unit 2: 상호작용 Dice Roller 앱 만들기 (0) | 2022.11.23 |