MAD Skills 앱 아키텍쳐 - UI 레이어

Architecture: The UI layer - MAD Skills (영상, 문서)

💡개념이 헷갈리거나 제가 잘 모르거나 많은 사람들이 잘 모를 것 같은 것 위주로 정리

UI 레이어

레이어드 구조

  • UI Layer
    • UI elements
    • State holders
  • Domain Layer
  • Data Layer

ui-layer

역할

  • Data 레이어에서 가져온 애플리케이션 상태를 시각적으로 나타낸다.
  • 애플리케이션 데이터 변경사항을 UI가 표시할 수 있는 형식으로 변환한 후에 표시한다.

UI 상태

역할

UI 상태를 담는 클래스

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Immutable

동일한 정보의 수정은 데이터 원천 위치를 제외하고 어느 곳에서도 하면 안 된다. 데이터를 아무곳에서나 수정한다면 동일한 정보의 원천 위치가 여러 곳이 되어 데이터 불일치와 버그가 발생할 수 있다.

이름 규칙

기능 + UiState

UDF(Uni-directional Flow)

동작 흐름

udf

이점

  • UI용 정보 소스가 하나로 유지되기 때문에 데이터 일관성을 만족한다.
  • 상태 소스가 분리되므로 UI와 별개로 테스트할 수 있다.
  • 사용자 이벤트의 처리 흐름을 파악하기 쉬워 유지보수하기 편하다.

동작 흐름 안에서 로직 구분

비지니스 로직

상태 변경에 따라 진행해야 할 작업

Domain 또는 Data 레이어에 위치한다.

UI 로직

상태 변경사항을 표하시는 방법

보통 ViewModel에서 구현하지만 Context 같은 Android 요소를 사용하는 경우 testable을 위해 UI의 State holder에서 구현하기도 한다.

관찰 가능한 UI 상태

관찰 가능한 상태로 노출하기

ViewModel에서 데이터를 직접 가져오지 않고도 UI가 상태 변경사항에 반응할 수 있도록 하기 위해 관찰 가능한 상태 데이터를 노출한다.

class NewsViewModel(
        private val getNewsItemsForCategory: GetNewsItemsForCategory,
        ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = getNewsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

단일 상태 스트림 vs 여러 상태 스트림

단일 상태 스트림의 장점

  • 편의성
    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    
  • 데이터 일관성

여러 상태 스트림을 선택해야 할 때

  • 서로 관련 없는 UI 데이를 번들로 묶는 데 드는 비용이 큰 경우
  • 필드가 많으면서 자주 업데이트 되는 필드가 있는 경우

관찰 수명주기

관찰 가능한 상태를 사용할 땐 UI의 수명주기를 고려해야 한다.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}