Study Record

[Android] Compose ViewModel 본문

안드로이드/compose

[Android] Compose ViewModel

초코초코초코 2023. 9. 13. 14:40
728x90

😶 gradle 추가

build.gradle.kts(Module: app) 을 열고 dependencies 에 ViewModel 종속 항목을 추가한다.

dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}

 

 

😶 설계

권장 앱 아키텍처에 따르면 먼저 앱 기능에 따라 사용될 데이터를 정의하고 그 데이터에 따라 UI를 설계하는 것이 원칙이라고 한다. 그 원칙에 따라 로그인 기능을 만들려고 한다. 로그인 기능을 위한 데이터는 이메일 주소와 비밀번호다. 

 

데이터에 따라 UI는 이메일 주소와 비밀번호를 사용자가 입력할 수 있는 입력 폼 2개와 제출하는 버튼이 하나 필요하다. 디자인을 고려해 완성된 UI 도안은 다음과 같을 것이다.

 

로그인을 위한 화면이 하나 필요할 것이고 이름은 LoginScreen 라고 지었다. 권장 앱 아키텍처에 따라 UI 요소와 UI 상태를 설계할 차례다. UI 요소는 Compose 를 사용할 것이고 UI 상태는 로그인 화면에서 사용자가 봐야한다고 지정하는 항목이므로 email 과 password 그리고 제출 버튼을 누를 수 있는지 없는지에 대한 Flag 가 될 수 있다.

 

UI 요소

Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White)
            .padding(horizontal = 24.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        OutlinedTextField(
            value = "",
            onValueChange = {},
            label = { Text(text = "email") }
        )
        Spacer(modifier = Modifier.height(16.dp))
        OutlinedTextField(
            value = "",
            onValueChange = {},
            label = { Text(text = "password") }
        )
        Spacer(modifier = Modifier.height(24.dp))
        Button(
            onClick = {},
            enabled = false
        ) {
            Text(text = "submit")
        }
    }

 

UI 상태

email 과 password 그리고 제출버튼에 대한 Flag 를 포함한다.

data class LoginUiState(
    val email: String = "",
    val password: String = "",
    val isSubmit: Boolean = false
)

 

 

UI 요소와 UI 상태를 하나의 단일 클래스에서 구현하고 관리할 수 있지만 그러면 점점 복잡해져 이도 저도 알 수 없게 될수도 있다. 이를 위해 우리는 상태 홀더라는 개념을 사용할 수 있다. 상태 홀더는 UI 상태를 생성하는 역할을 담당하고 UI 상태와 관련된 작업을 위한 로직을 담고 있다. 즉, 상태 홀더는 데이터를 보유하고 UI 에 데이터를 노출하며 앱 로직을 처리하는 역할을 한다. ViewModel 은 상태 홀더 중 하나로 UI 상태를 관리하는 클래스이다.

 

상태 홀더(ViewModel)

그렇다면, 로그인 기능에 필요한 데이터는 UI 상태를 통해 알았으니 UI 상태와 관련된 로직이 존재할 것이다. 로그인 기능이기 때문에 사용자가 email 을 입력하면 UI 상태를 업데이트하는 로직, 마찬가지로 password 를 입력하면 UI 상태를 업데이트하는 로직, 제출 버튼이 활성과 관련된 로직, 제출 버튼을 눌렀을 때의 기능에 대한 로직을 상상할 수 있다.

 

이 모든 걸 고려하여 만든 LoginViewModel 은 다음과 같다.

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> get() = _uiState.asStateFlow()


    fun updateEmail(newEmail: String) {
        _uiState.update {
            it.copy(
                email = newEmail
            )
        }
        checkSubmit()
    }

    fun updatePassword(newPassword: String) {
        _uiState.update {
            it.copy(
                password = newPassword
            )
        }
        checkSubmit()
    }

    fun loginSubmit() {
        /* login Submit */
    }

    private fun checkSubmit() {
        _uiState.update {
            it.copy(
                isSubmit = _uiState.value.email != "" && _uiState.value.password != ""
            )
        }
    }
}

 

StateFlow

ViewModel 에서 사용한 StateFlow 는 현재 상태와 새로운 상태 업데이트를 내보내는 관찰 가능한 데이터 홀더 흐름이다. StateFlow 의 value 속성은 현재 상태 값을 반영하는데 상태를 업데이트하고 흐름에 전송하려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당한다.

 

Composable 함수가 UI 를 업데이트하는 방법은 상태(State)가 변경되어 리컴포저블이 예약되어 Composable 함수가 다시 빌드되는 방법이다. 다크 코드에서 라이트모드로 변경하거나 가로 모드에서 세로 모드로 변경하는 등 구성 변경이 일어났을 때 데이터를 유지해야 한다.

 

StateFlow 는 이모든 조건을 만족한다. 다음과 같이 uiState.collectAsState() 함수는 StateFlow 에서 값을 수집하고 State 를 통해 최신 값을 나타낸다. StateFlow.value 가 초기값으로 사용되며  StateFlow 에 새 값이 게시될 때마다(ex. _uiState.update{}) 반환된 State 가 업데이트되어 State.value 사용된 모든 경우에서 리컴포저블이 이루어진다.

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> get() = _uiState.asStateFlow()
    
    fun updateEmail(newEmail: String) {
        _uiState.update {
            it.copy(
                email = newEmail
            )
        }
        checkSubmit()
    }
    /* 앱 로직 */
}

@Composable
fun LoginScreen(
    loginViewModel: LoginViewModel = viewModel()
) {
    val loginUiState by loginViewModel.uiState.collectAsState()
    
	/* UI 요소 */
}

 

 

LoginScreen

ViewModel 과 UI 상태를 고려하여 LoginScreen 함수를 완성하면 다음과 같다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
    loginViewModel: LoginViewModel = viewModel()
) {
    val loginUiState by loginViewModel.uiState.collectAsState()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White)
            .padding(top = 120.dp, end = 20.dp, start = 20.dp)
            .verticalScroll(rememberScrollState()),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        OutlinedTextField(
            value = loginUiState.email,
            onValueChange = loginViewModel::updateEmail,
            label = { Text(text = "email", color = Color.Black) },
            colors = TextFieldDefaults.outlinedTextFieldColors(
                textColor = Color.Black
            )
        )
        Spacer(modifier = Modifier.height(16.dp))
        OutlinedTextField(
            value = loginUiState.password,
            onValueChange = loginViewModel::updatePassword,
            label = { Text(text = "password", color = Color.Black) },
            colors = TextFieldDefaults.outlinedTextFieldColors(
                textColor = Color.Black
            )
        )
        Spacer(modifier = Modifier.height(24.dp))
        Button(
            onClick = loginViewModel::loginSubmit,
            enabled = loginUiState.isSubmit,
            colors = ButtonDefaults.buttonColors(
                containerColor = Color(0xFF0039AD),
                disabledContainerColor = Color(0xFFA4C2FF),
                contentColor = Color(0xFF000D3A),
                disabledContentColor = Color.White
            )
        ) {
            Text(text = "submit")
        }
    }
}

 

결과 동영상

 

 

 

728x90

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

[Android] Compose 다이얼로그  (0) 2023.09.13
[Android] Compose 단위 테스트  (0) 2023.09.13
[Android] Compose 애니메이션 효과  (0) 2023.09.06
[Android] Compose Appbar  (0) 2023.09.04
[Android] Compose 테마  (0) 2023.09.04