Android Ui 상태 표현하는 여러가지 방법

송훈기·2022년 10월 30일
0

개요

https://developer.android.com/topic/architecture/ui-layer

안드로이드 개발문서 중 아키텍처와 관련된 내용을 다루고 있는 페이지 입니다.
아키텍처의 레이어 중에서 Ui 레이어에 대한 설명이 자세하게 나와 있으니 이를 확인하시면 좋을 거 같습니다.
이 글은 위의 글을 읽고 생각을 정리하고 다양한 방법을 적어놓은 글입니다.

내용

프로젝트를 진행하면서 느끼는 거지만 UI는 확실히 단편적인 것이 좋은거 같다.
안드로이드 프로그래밍을 하다보면 많은 스트림(Flow, Observable, etc..)들이 생겨나고, 스트림들을 엮어서 다른 상태의 스트림을 다시 만들어낸다 (버튼의 활성화 조건, 혹은 여러가지 이벤트)
이 방법은 확실히 규모가 커지고, 화면 내에서 체크해야 하는 조건이 많아질 수록 가독성은 떨어지는 것 같다.
예를 한번 들어보자
아이디비밀번호를 입력받고 원하는 조건이 성립하면 버튼을 활성화 해주는 로직을 ViewModel에 작성한다고 해보자
TwowayDataBindingStateFlow (혹은 LiveData)를 잘 알고 있는 개발자라면 이와 같이 작성할 수 있다.

class MainViewModel : ViewModel() {

    var idState = MutableStateFlow("")

    var passwordState = MutableStateFlow("")

    val enableState = combine(
        idState,
        passwordState
    ) { id, password ->
        id.length in 1..10 && password.length in 2..10
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000L),
        initialValue = false
    )
}

StateFlow는 항상 최신의 값을 emit할 수 있도록, BufferSizereplay로 1를 걸어놨기 때문에 언제든 사용자의 인터랙션에 변화해 최신의 값을 반환한다.
그러므로 이 로직자체에서 사이드 이펙트를 찾기는 어렵다
물론 감춰지지 않은 var 필드의 setter를 이상한 레이어에서 사용한다면..그것도 사이드 이펙트라면 어마어마한 사이드 이펙트가 존재하긴 한다.
그러나 화면에서 조건이 늘어나서, 주소란과 상세 주소란이 생기고 전화번호까지 생겨난다면 어떨까?
그 조건에서 버튼의 활성 조건이 더욱 세분화 되고 다양해 진다면, combine(엮어진) 스트림인 enableState는 아마...처참해질 것이다.

그래서 최근 이러한 무분별한 스트림의 생성을 어떻게 하면 가독성이 좋게, 할 수 있을까 생각하다가 공식문서를 확인할 수 있었고, 그곳에서 새로운 방법을 찾아내었다.


class Main2ViewModel : ViewModel() {
    var uiState = MutableStateFlow(Main2UiState())
        private set
        
    fun updateId(id: String) {
        uiState.value = uiState.value.copy(id = id)
        updateButtonEnable()
    }

    fun updatePassword(password: String) {
        uiState.value = uiState.value.copy(password = password)
        updateButtonEnable()
    }

    private fun updateButtonEnable() {
        uiState.value = uiState.value.copy(
            buttonEnable = ((uiState.value.id.length in 1..10) && (uiState.value.password.length in 2..10))
        )
    }
}

data class Main2UiState(
    val id: String = "",
    val password: String = "",
    val buttonEnable: Boolean = false
)

결론부터 얘기하자면, 화면의 상태(State)를 나타내는 단 1개의 스트림을 두는 것이 좋지 않을까라고 생각했고, 공식문서에서도 이와 같은 방식을 권장했다.
이렇게 된다면, ui가 변경되는 변경지점을 제한할 수 있고, 무분별하게 스트림이 파생되어 가독성이 떨어지는 경우는 없을 것이다.
물론, 이게 완벽하다거나 규모가 늘어났을 때 가독성이 좋아진다!는 잘 모르겠다. 하지만 적어도 상태를 나타내는 단 하나의 스트림만 확인하고 이곳에서 부터 타고타고 가는 방식은 프로젝트에 대해 전혀 모르는 개발자가 보아도 이해할 수 있지 않을까 생각한다.
그외 뭐...변경가능성을 줄이고 이런 저런 여러가지 장점이 존재하는데 이는 위에 있는 공식문서를 확인하면 감사하겠다.

<개인적 의견>
이 방법이 오히려 fragment나 혹은 Compose에서 arguments를 받아서 처리해야하는 로직을 작성할 때 더 가독성이 좋다고 생각한다.

class Main2ViewModel @Inject constructor(
   private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    var uiState = MutableStateFlow(Main2UiState())
        private set
    
    // (1)
    init {
       savedStateHandle.get<String>("transferData")?.let {
           uiState.value = uiState.value.copy(id = it)
       }
    }
        
    fun updateId(id: String) {
        uiState.value = uiState.value.copy(id = id)
        updateButtonEnable()
    }

    fun updatePassword(password: String) {
        uiState.value = uiState.value.copy(password = password)
        updateButtonEnable()
    }

    private fun updateButtonEnable() {
        uiState.value = uiState.value.copy(
            buttonEnable = ((uiState.value.id.length in 1..10) && (uiState.value.password.length in 2..10))
        )
    }
}

data class Main2UiState(
    val id: String = "",
    val password: String = "",
    val buttonEnable: Boolean = false
)

1번과 같은 식으로 표현이 가능해지는데, 만약 개별 스트림으로 작성해서 표현한다면

class MainViewModel @Inject constructor(
   private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    val transferData = savedStateHandle.getStateFlow<String>("transferData")

    var idState = MutableStateFlow("")
    
    val combined = combine(transferData, idState) { data, id -> ... }

    var passwordState = MutableStateFlow("")

    val enableState = combine(
        idState,
        passwordState
    ) { id, password ->
        id.length in 1..10 && password.length in 2..10
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000L),
        initialValue = false
    )
}

이런식으로 작성이 되는데, 나의 경우엔 첫번째 방법이 좀 더 보기에 좋지 않나 싶긴하다.

앞으로 이러한 내용을 표현하는 다양한 방법이 존재한다면 이곳에 적어두려고 한다.
예제 코드는 아래 링크에서 확인할 수 있습니다.

https://github.com/SSong-developSampleAndroid/SampleUiState

profile
안녕하세요 송훈기입니다.

0개의 댓글