안드로이드 연구소

[안드로이드 클린 아키텍처] 안드로이드 앱 아키텍처 가이드라인(UI레이어) 본문

안드로이드 연구소/클린아키텍처

[안드로이드 클린 아키텍처] 안드로이드 앱 아키텍처 가이드라인(UI레이어)

안드로이드 연구원 2023. 6. 16. 17:40

Q1. UI 레이어에 대해 설명해줘

https://developer.android.com/jetpack/guide/ui-layer?hl=ko#case-study 

UI레이어는 데이터 레이어에서 가져온 애플리케이션 상태를 시각적으로 나타냅니다.

위에서 설명한 Data레이어의 레파지토리와 이곳에서 연결이되어서 UI로 나타나는 것 같습니다.

 

 

Q1-1. UI레이어는 어떻게 구성되어있니?

UI레이어로는 UI state와 UI element 두 구성요소가 있습니다.

UI element는 위젯이라고도하며 애플리케이션의 사용자가 화면에서 보고 상호 작용할 수 있는 요소입니다.
TextView, EditText, Buttons은 UI element에 해당됩니다.

UI state
는 UI element가 화면에 표시되고 작동하는 방식을 결정하는 구성과 속성을 나타내는 데이터입니다
TextView의 텍스트 데이터나 출력 여부를 결정하는 데이터는 UI state에 해당합니다.

 

 

Q1-2. Data레이어에서 UI레이어는 어떻게 상호작용하지?

1. 먼저 데이터 레이어에서 UI와 관련된 데이터를 가져옵니다.
2. 그 후 UI State holder클래스에서 데이터 레이어에서 받은 데이터를 가공하여 UI state로 만드는 역할을 합니다.
(* 안드로이드에서는 ViewModel에서 UI state holder역할을 하여 데이터를 가공합니다.)
3. 버튼 클릭과 같은 UI event가 발생하면 viewModel을 통해 데이터 레이어의 해당 데이터가 변경됩니다.

4. 1,2,3번이 반복됩니다. 

이때 Jetpack Compose를 사용하면 UI state의 관리 및 전파를 간소화하여 최적화할 수 있다고 합니다.

UI state를 관리하는 holder클래스는 주로 ViewModel이 그 역할을 하고 있고

ViewModel은 Data레이어와 UI레이어간의 중요한 연결점인 것 같습니다.

 

Q1-3. UI state 데이터를 담는 클래스 예제를 보여줘

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,
    ...
)

 

Q1-4. Data 레이어의 데이터를 ViewModel와 접근하는 예제를 보여줘

[case1] 코틀린 Flow의 stateFlow를 사용하는 예제

class NewsViewModel(
    private val repository: NewsRepository, // (0)Data레이어의 레파지토리 의존성 주입
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()// (1) stateFlow를 사용하여 UI state노출

    private var fetchJob: Job? = null // (2) 비동기 작업 세팅

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                // 성공시 UI element 업데이트
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
                // 실패시 UI element 업데이트
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

[case2] LiveData사용시

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableLiveData<NewsUiState>()
    val uiState: LiveData<NewsUiState> = _uiState

    private var fetchJob: Job? = null

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

[case3] Compose 사용하기

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

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

 

 

Q4-5. 엑티비티에서 ViewModel의 UI state와 접근하는 예제입니다.

[case1] Kotlin Flow 사용하기

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
                }
            }
        }
    }
}

uiState메서드를 사용하면 ViewModel내의 속성이 보유하는 ui state를 사용할 수 있습니다.

그리고 kotlin Flow의 메서드인 collect를 사용하여 ui state데이터를 수집합니다.

수집한 ui state로 UI를 업데이트하면 되는거겠죠?

또 kotlin Flow를 사용하면 생명주기를 관리해주어야합니다.

 

[case2] LiveData 사용하기

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

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

        viewModel.uiState.observe(this) { state ->
            // Update UI elements based on the state
        }
    }
}

기존의 LiveData는 엑티비티나 프레그먼트에서 observe를 하지않고 ViewModel에서 사용하고 있네요.

또 마찬가지로 uiState메서드를 사용하면 ViewModel내의 속성이 보유하는 ui state를 사용할 수 있습니다.

그리고 observe로 관찰하여 요소들을 변화에 UI를 업데이트합니다.
 LiveData를 사용하면 엑티비티에 따라 생명주기가 맞춰지니 메모리 누수 또한 걱정이 없겠죠?

 

case3: Jetpack Compose 사용하기

@Composable
fun LatestNewsScreen(viewModel: NewsViewModel = viewModel()) {
    // Show UI elements based on the viewModel.uiState
}

와우 Jetpack Compose를 사용해본적이 없지만

해당 기술을 사용하면 따로 설정없이 UI state를 사용할 수 있네요.

 


UI레이어를 정리하자면

첫번째, UI state와 UI element를 관심사 분리해야합니다.
그렇다면 코드 모듈성과 유지 관리성을 개선하고, 테스트 가능성을 향상시킬 수 있습니다.
그러기 위해서는 ViewModel에서 uiState를 잘 설정해두어야겠군요.

두번째, 엑티비티에서 VIewModel의 UI state를 관찰해야합니다.
위에서 보면 Jetpack Compose는 viewModel.uiState에 대한 기본적으로 세팅이 되어 있어 사용하기가 용이합니다.
공식적으로 클린 아키텍처 가이드를 제공하여 Jetpack compose의 사용을 많은 개발자에게 독려할 수 있을 것으로 보입니다.

 

다음 포스팅에서 계속

Comments