컴포즈에서 상태는 어떻게 정의하고 사용하는지 알아보는 코드랩을 진행하였다. 코드를 보면서 전체적인 이해를 해보도록 하자.
Composable 에서의 상태
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
Composable 함수 (WaterCounter 함수)를 사용하여 새 파일 WaterCounter.kt 를 생성 한다.
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
기본 화면을 나타내는 WellnessScreen.kt 파일을 만들고 WaterCounter 함수를 호출한다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
WellnessScreen()
}
}
}
}
}
메인 액티비티 에서 이를 메인 화면으로 호출한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
WaterCounter 를 Column
으로 포함하고, 버튼 클릭 시 마다 카운트가 갱신 되도록 설정한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
mutableStateOf
으로 카운트를 수정한다. 하지만 수정이 반영되지 않는다. 왜냐하면 리컴포지션이 일어나면서 동시에 다시 count
변수가 0으로 초기화 되기 때문이다.
remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) } //+
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
remember
를 사용하여 상태를 정의하면 리컴포지션 시에도 이를 유지할 수 있다.
by 델리게이트
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) } //+
Text("You've had $count glasses.") //+
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) { //+
Text("Add one")
}
}
}
by
델리게이트를 사용하여 상태를 정의, 변수명에 바로 접근하여 값을 수정 가능하다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
한번이라도 버튼을 클릭해야 ui가 존재하도록 설정한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
버튼이 10 미만 일 때만 활성화 되도록 설정
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
카운트가 1이상이면, TaskItem 을 표시
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
닫은 경우 task 를 미표시 하도록 함.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
카운트 클리어 버튼 추가, 이때 카운트가 0으로 클리어 되면서 if(count>0)
라인틀 타지 않아 showTask
remember 변수가 제거 된다.
rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by rememberSaveable { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
rememberSaveable
을 사용하면 구성변경 시에도 상태를 유지할 수 있다.
Stateless / Stateful
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatelessCounter 상태를 갖지 않는 컴포저블
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
StatefulCounter 상태를 갖는 컴포저블
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
stateful 한 컴포저블 사용하기
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter()
}
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
상태를 가지지 않는 컴포저블을 재사용하는 예시 (StatelessCounter
)
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
각각 상태를 가지는 WellnessTaskItem
과 아닌 WellnessTaskItem
상태를 가지지 않는 컴포저블의 경우에는 상태에 대한 콜백을 함수 파라미터로 받으며, 상태를 상위로 끌어올렸다.
observable 한 list
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
찰가능한 MutableList (list.remove 를 알아챈다)
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit, // list 를 상위 수준으로 호이스팅 했기 때문에, 람다로 close 콜백을 받는다.
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
list 를 상위 수준으로 호이스팅 했기 때문에, 람다로 close 콜백을 받는다.
참고로 rememberSavable 로 리스트를 저장하는 경우에는 에러가 발생한다. (커스텀 save 기능을 정의해 주어야 하는데, 큰 데이터의 경우 Saveable 로 저장하는 것은 권장되지 않는다.)
ViewModel 사용
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
ViewModel (MutableStateList 와 이를 수정하는 메서드를 갖는 뷰모델)
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
viewModel compose dependencies 추가
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
WellnessScreen 에서 뷰모델 사용하기. viewModel()은 기존 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성합니다.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
WellnessTask
데이터 클래스에 체크 상태를 추가한다.
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
viewModel 에 체크 상태를 바꾸는 메서드 추가하고, 이전에 작성하였던 stateful 컴포저블을 제거한다.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
위 처럼 전반적 리팩토링을 해주고, 이제 뷰 모델로 부터 콜백을 제공받을 것이다.
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
뷰 모델 로부터 콜백을 구현한다. 하지만 위 코드에는 버그가 있다 무엇일까?
바로, Boolean 이기 때문에 재구성이 일어나지 않는다. (MutableState 를 사용해야한다.)
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
/**
* WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경합니다. WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있습니다.
*/
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경합니다. WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있습니다.
ReadMe.md
로직 유형 2가지
비즈니스 로직 이란?
비즈니스 로직은 상태 변경 시(예: 결제하기 또는 사용자 환경설정 저장) 실행할 작업입니다. 이 로직은 대개 비즈니스 레이어나 데이터 영역에 배치되고 UI 레이어에는 배치되지 않습니다.
UI 로직 이란?
UI 로직은 화면에 상태 변경을 표시하는 방법(예: 탐색 로직 또는 스낵바 표시)과 관련이 있습니다.
ViewModel
뷰모델의 역할
- ViewModel은 UI 상태와 앱의 다른 레이어에 있는 비즈니스 로직에 대한 액세스 권한을 제공합니다.
끈질긴 뷰모델의 생명주기
- ViewModel은 구성 변경 후에도 유지되므로 컴포지션보다 전체 기간이 더 깁니다.
- Compose 콘텐츠 호스트의 수명 주기(즉, 활동이나 프래그먼트, Compose Navigation을 사용하는 경우 탐색 그래프의 대상)를 따를 수 있습니다.
Compose 에서 ViewModel 사용 시 주의 점
- 경고: ViewModel은 컴포지션의 일부가 아닙니다. 따라서 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 상태(예: 기억된 값)를 보유해서는 안 됩니다.
Compose 의 viewModel()
- viewModel()은 기존 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성합니다.
- ViewModel 인스턴스는 범위가 활성화되어 있는 동안 유지됩니다.
- 예를 들어 컴포저블이 활동에서 사용되는 경우 viewModel()은 활동이 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환합니다.
주의 : 뷰모델을 다른 컴포저블로 전달해서는 안됩니다.
- ViewModel은 탐색 그래프의 활동이나 프래그먼트, 대상에서 호출되는 루트 컴포저블에 가까운 화면 수준 컴포저블에서 사용하는 것이 좋습니다.
- ViewModel은 다른 컴포저블로 전달하면 안 됩니다. 대신 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 합니다.
No newline at end of file
Reference
2022.10.15 - [Android/Google Play] - 구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션
2022.10.09 - [Android/Groovy] - [Android Groovy] build.gradle plugins apply false? 왜 false 인가
'Android > Jetpack Compose' 카테고리의 다른 글
[Jetpack Compose] 애니메이션 활용 및 기본 내용 정리 (0) | 2022.12.17 |
---|---|
[Jetpack Compose] view + xml 기반에서 컴포즈로 마이그레이션 (1) | 2022.12.16 |
[Jetpack Compose] 테마 Theming 기본 정리 (0) | 2022.12.15 |
[Android] This annotation should be used with the compiler argument '-opt-in=kotlin.RequiresOptIn' 해결하기 (0) | 2022.10.09 |
[Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈) (0) | 2021.12.18 |