Jetpack Compose 상태 관리 학습

1. 상태 없는 컴포넌트

1.1 의존성 추가

implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.0")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.material3:material3")

implementation("com.google.android.material:material:1.11.0")
implementation("androidx.compose.material:material-icons-extended:1.5.4")
implementation("androidx.compose.runtime:runtime-livedata:1.5.4")
implementation("androidx.compose.runtime:runtime:1.5.4")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

1.2 데이터 클래스

package com.example.composestate.todo

import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CropSquare
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.RestoreFromTrash
import androidx.compose.material.icons.filled.Square
import androidx.compose.ui.graphics.vector.ImageVector
import com.example.composestate.R
import java.util.UUID

data class TaskItem(val title:String, val icon:TaskIcon = TaskIcon.Default, val id:UUID=UUID.randomUUID())

enum class TaskIcon(
    val imageVector: ImageVector,
    @StringRes val contentDescription: Int
){
    Square(Icons.Default.CropSquare, R.string.cd_expand),
    Done(Icons.Default.Done, R.string.cd_done),
    Event(Icons.Default.Event, R.string.cd_event),
    Privacy(Icons.Default.PrivacyTip, R.string.cd_privacy),
    Trash(Icons.Default.RestoreFromTrash, R.string.cd_restore);

    companion object {
        val Default = Square
    }
}

1.3 UI 컴포넌트

package com.example.composestate.todo.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.composestate.todo.TaskItem

@Composable
fun TaskListScreen(
    tasks:List<TaskItem>
){
    Column() {
        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp)
        ){
            items(tasks){
                TaskRow(task = it, modifier = Modifier.fillParentMaxWidth())
            }
        }
        Button(onClick = {
            // 추가 로직
        },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text(text = "랜덤 항목 추가" )
        }
    }
}

@Composable
fun TaskRow(
    task: TaskItem,
    modifier: Modifier = Modifier
){
    Row(
        modifier = modifier
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(text = task.title)
        Icon(imageVector = task.icon.imageVector
            ,contentDescription = stringResource(id = task.icon.contentDescription))
    }
}

1.4 ViewModel

package com.example.composestate.todo.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.composestate.todo.TaskItem

class TaskViewModel:ViewModel(){
    private var _taskList = MutableLiveData(listOf<TaskItem>())
    val taskList:LiveData = _taskList

    fun addTask(item:TaskItem){
        _taskList.value = _taskList.value!! + listOf(item)
    }

    fun removeTask(item:TaskItem){
        _taskList.value = _taskList.value!!.toMutableList().also {
            it.remove(item)
        }
    }
}

1.5 상태 관리 및 재구성

package com.example.composestate.todo.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.composestate.todo.TaskItem

@Composable
fun TaskListScreen(
    tasks:List<TaskItem>,
    onAddTask: (TaskItem) ->Unit,
    onRemoveTask: (TaskItem) ->Unit
){
    Column() {
        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp)
        ){
            items(tasks){
                TaskRow(
                    task = it, 
                    modifier = Modifier.fillParentMaxWidth(), 
                    onTaskClicked = {onRemoveTask(it)}
                )
            }
        }
        Button(onClick = {
            onAddTask(generateRandomTask())
        },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text(text = "랜덤 항목 추가" )
        }
    }
}

@Composable
fun TaskRow(
    task: TaskItem,
    onTaskClicked:(TaskItem) -> Unit,
    modifier: Modifier = Modifier,
){
    Row(
        modifier = modifier
            .clickable{
                onTaskClicked(task)
            }
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(text = task.title)
        Icon(imageVector = task.icon.imageVector
            ,contentDescription = stringResource(id = task.icon.contentDescription))
    }
}

1.6 액티비티

package com.example.composestate.todo.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.example.composestate.todo.TaskIcon
import com.example.composestate.todo.TaskItem
import com.example.composestate.ui.theme.ComposeStateTheme

class TaskActivity : ComponentActivity() {
    private val taskViewModel by viewModels<TaskViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeStateTheme {
                TaskListScreen()
            }
        }
    }

    @Composable
    private fun TaskListScreen() {
        val tasks:List<TaskItem> by taskViewModel.taskList.observeAsState(initial = listOf())

        TaskListScreen(
            tasks=tasks,
            onAddTask = {  taskViewModel.addTask(it) },
            onRemoveTask = {taskViewModel.removeTask(it)}
        )
    }
}

1.7 재구성(Recomposition)

Jetpack Compose에서 재구성은 상태가 변경될 때 UI를 업데이트하는 메커니즘입니다. 상태가 변경되면 Compose는 해당 상태를 사용하는 컴포넌트를 다시 그립니다. 이 과정에서 불필요한 재구성을 최소화하는 것이 성능에 중요합니다.

package com.example.composestate.todo.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.example.composestate.todo.TaskItem

@Composable
fun OptimizedTaskListScreen(
    tasks:List<TaskItem>,
    onAddTask: (TaskItem) ->Unit,
    onRemoveTask: (TaskItem) ->Unit
){
    Column {
        LazyColumn(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp)
        ){
            items(tasks, key = { it.id }) { task ->
                var isExpanded by remember { mutableStateOf(false) }
                
                TaskRow(
                    task = task,
                    modifier = Modifier.fillParentMaxWidth(),
                    onTaskClicked = { onRemoveTask(task) },
                    isExpanded = isExpanded,
                    onExpandClicked = { isExpanded = !isExpanded }
                )
            }
        }
        Button(onClick = {
            onAddTask(generateRandomTask())
        },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text(text = "랜덤 항목 추가" )
        }
    }
}

@Composable
fun TaskRow(
    task: TaskItem,
    onTaskClicked:(TaskItem) -> Unit,
    isExpanded:Boolean,
    onExpandClicked:() -> Unit,
    modifier: Modifier = Modifier,
){
    Row(
        modifier = modifier
            .clickable{
                onTaskClicked(task)
            }
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(text = task.title)
        Row {
            Icon(
                imageVector = task.icon.imageVector,
                contentDescription = stringResource(id = task.icon.contentDescription),
                modifier = Modifier
                    .size(24.dp)
                    .clip(RoundedCornerShape(4.dp))
                    .clickable { onExpandClicked() }
            )
        }
    }
}

태그: Jetpack Compose 상태 관리 ViewModel LiveData Compose UI

5월 22일 01:38에 게시됨