UI 이벤트는 UI 레이어에서 UI 또는 ViewModel로 처리해야 하는 작업입니다. 가장 일반적인 이벤트 유형은 사용자 이벤트입니다. 사용자는 화면 탭하기 또는 동작 생성과 같은 앱과의 상호작용을 통해 사용자 이벤트를 생성합니다. 그러면 UI에서 onClick() 리스너와 같은 콜백을 통해 이러한 이벤트를 사용합니다
핵심 용어:
ViewModel은 일반적으로 사용자의 비지니스 로직을 처리합니다.(예: 사용자가 데이터를 새로고침하는 경우) 사용자 이벤트에는 UI에서 직접 처리할수있는 UI 동작 로직이 있을 수도 있습니다. (예: 다른화면 이동, snackbar 표시)

데이터에 영향이 없는 뷰가 보이고 안보이고처리는 UI에서 처리하고
데이터에 영향이 가는 갱신 기능은 ViewModel에서 처리한다.
class LatestNewsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLatestNewsBinding
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
// The expand details event is processed by the UI that
// modifies a View's internal state.
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// The refresh event is processed by the ViewModel that is in charge
// of the business logic.
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
}
}
RecyclerView 항목 또는 맞춤 VIew와 같이 UI 트리 아래쪽에서 작업되는 경우도 ViewModel을 통해 사용자 이벤트를 처리한다.
예를 들어 NewsActivity 의 모든 뉴스 항목에 북마크가 있다고 가정한다. RecyclerView Adapter의 북마크 버튼을 누를때 ViewModel의 addBookmark함수를 호출하지 않으며, ViewModel의 종속 항목이 필요한다. 대신 ViewModel은 이벤트 처리를 위한 구현이 포함된 NewsItemUiState라는 상태 객체를 노출한다.
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
val publicationDate: String,
val onBookmark: () -> Unit
)
class LatestNewsViewModel(
private val formatDateUseCase: FormatDateUseCase,
private val repository: NewsRepository
)
val newsListUiItems = repository.latestNews.map { news ->
NewsItemUiState(
title = news.title,
body = news.body,
bookmarked = news.bookmarked,
publicationDate = formatDateUseCase(news.publicationDate),
// Business logic is passed as a lambda function that the
// UI calls on click events.
onBookmark = {
repository.addBookmark(news.id)
}
)
}
}
이렇게하면 RecyclerView 어댑터가 NewsItemUiState 객체 목록과 같이 필요한 데이터만 사용할 수 있다. 어댑터가 전체ViewModel에 억세스 할 수 없으므로 VIewModel에 의해 노출된 기능을 악용할 가능성이 낮다. Activity클래스에서만 ViewModel을 사용하도록 허용하는 경우 책임이 분리된다.
ViewModel 함수는 처리하는 작업에 따라 동사를 포함해 이름이 지정됩니다(예: addBookmark(id) 또는 logIn(username, password)).
ViewModel에서 발생하는 Ui 작업은 항상 UI 상태 업데이트로 이뤄진다. -> 데이터 단방향 흐름 원칙에 의해 구성 변경후 이벤트를 재현할 수 있으며 UI 작업이 손실되지 않는다.
로그인후 홈 화면으로 이동하는 경우 코드
(corutine를 수명주기와 함께 사용하는 방법 추후 공부 예정)
data class LoginUiState(
val isLoading: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
/* ... */
}
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
UI에서 특정 ViewModel 이벤트를 소비하면 다른 UI 상태가 업데이트될 수 있습니다.
예) 화면에 임시 메시지를 표시하여 사용자에게 무언가 발생했음을 알리는 경우 (데이터 상태에 따라 메시지가 표시되고 닫히기 때문에 ViewModel을 통해서 모델링한다.
// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
val news: List<News> = emptyList(),
val isLoading: Boolean = false,
val userMessage: String? = null
)
비즈니스 로직이 사용자에게 임시 메시지를 새로 표시해야 하는 경우 ViewModel은 다음과 같이 UI 상태를 업데이트합니다.
class LatestNewsViewModel(/* ... */) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = "No Internet connection")
}
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
}
tip: viewModelScope: ViewModel이 destroy 될 때 자식 코루틴을 자동으로 취소하는 기능을 제공한다.
ViewModel은 UI가 화면에 메시지를 표시하는 방식을 알 필요가 없습니다. 표시해야 하는 사용자 메시지가 있다는 사실만 알면 됩니다. 임시 메시지가 표시되면 UI가 ViewModel에 이를 알려야 하며 그러면 userMessage 속성을 삭제하기 위해 또 다른 UI 상태 업데이트가 발생합니다.
class LatestNewsActivity : AppCompatActivity() {
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
uiState.userMessage?.let {
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
...
}
}
}
}
}
메시지가 일시적이더라도 UI 상태는 모든 시점에 화면에 표시되는 내용을 충실하게 표현합니다. 사용자 메시지는 표시되거나 표시되지 않습니다.
이벤트 소비로 상태 업데이트 트리거 가능 섹션에서는 UI 상태를 사용하여 화면에 사용자 메시지를 표시하는 방법을 자세히 설명합니다. 탐색 이벤트는 Android 앱의 일반적인 이벤트 유형이기도 합니다.
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.helpButton.setOnClickListener {
navController.navigate(...) // Open help screen
}
}
}
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
코드 이해가 안됨.
UI 상태 업데이트로 UI 이벤트 사용 사례를 해결할 수 없다고 생각되면 앱의 데이터 흐름 방식을 다시 고려해야 할 수도 있습니다. 다음 원칙을 고려하세요.