일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- appbar
- textview
- Dialog
- Coroutines
- textfield
- drift
- Kotlin
- 앱
- Navigation
- activity
- Flutter
- 계측
- LifeCycle
- DART
- Button
- android
- binding
- ScrollView
- viewmodel
- data
- CustomScrollView
- TEST
- livedata
- 안드로이드
- scroll
- tabbar
- Compose
- 앱바
- intent
- 테스트
- Today
- Total
Study Record
[Android] Compose ViewModel 본문
😶 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")
}
}
}
결과 동영상
'안드로이드 > 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 |