Jetpack Compose - Architecture(ViewModel)

2024. 2. 4. 19:10IT/Android

728x90

<미리 보기>

 

 

<소스 코드>

https://github.com/SeongHyunJeon/android-kotlin-practice/tree/6bf0307305882c35e1b7016b8a28422fe5b2bbd6/Unscramble

 

 

<정리>

아키텍처 원칙

1. 관심사 분리 - 각 기능의 구현이 독립적으로 존재하여 다른 부분에 영향을 주지 않도록 디자인하는 방법으로, 변경이 필요한 경우 해당 기능만 수정하면 되므로 다른 부분에 대한 걱정 없이 코드를 개선, 확장할 수 있다.

 

2. 모델에서 UI 만들기 - 앱의 데이터 처리를 담당하는 구성 요소인 모델을 통해서 UI를 완성시키는 방법으로, 이를 통해 데이터가 UI 요소와 독립되면서 수명 주기에 영향을 받지 않게 된다.


아키텍처 구조

이 그림이 모든 걸 설명한다.

1. UI 레이어 - 화면에 데이터를 출력하기 위한 요소들로, 크게 UI 요소와 상태 홀더로 나뉜다.

2. 데이터 레이어 - 데이터를 정의, 저장, 반환하는 요소들.

 

*UI 요소 - 화면에 데이터를 출력하는 구성 요소들을 의미하는데, 대표적으로 Jetpack Compose의 컴포저블 함수인 텍스트, 이미지, 버튼 등이 있다.

 

*상태 홀더 - 데이터를 보유하고 처리하여 UI에 노출시키는 구성 요소를 의미하는데, 대표적으로 뷰 모델(ViewModel)이 있다.


뷰 모델(ViewModel) - 모델과 UI 간 다리 역할을 담당한다. UI는 발생 가능한 이벤트를 통해 데이터를 요청하면 뷰 모델이 모델로 부터 데이터를 받아 처리하여 UI에 보내주는 식이다. 액티비티가 소멸되고 다시 생성될 때, 그러니까 구성 변경이 실행될 때 액티비티와 달리 ViewModel 객체는 소멸되지 않기 때문에 이런 특성에 근거하여 적합한 데이터는 저장될 수 있다.

 

UI 상태(UI state) - 앱과의 상호작용을 통해 변경될 수 있는 UI 요소에 출력되는 데이터를 의미한다.

 

StateFlow - 해당 타입으로 선언된 변수의 값이 업데이트 되면, 이 변수를 구독하여 '영향을 받은' 함수는 다시 실행하게 된다. 일반적으로 뷰 모델에서 UI 상태를 저장하는 변수의 타입으로 사용되며, 이를 UI 레이어에서 구독하여 UI 요소에 전달하게 된다. 이를 통해 UI 레이어는 항상 최신 상태의 데이터를 받아 화면에 출력할 수 있게 된다.

 

지원 속성(Backing Property) - 데이터가 클래스 내부에서만 변경될 수 있도록 사용하는 보조 프로퍼티로, 외부에서는 해당 데이터를 읽기 전용 프로퍼티를 통해 접근하게 된다. 세터의 접근 제어자를 private으로 설정한 것과 같다고 생각할 수 있는데, 밑의 코드를 보면 그 차이를 알 수 있다.

//참조하고 있는 객체를 변경할 수 없고, 그 객체의 속성 값도 변경할 수 없다. 
private val _uiState = MutableStateFlow(GameUiState())

//참조하고 있는 객체를 변경할 순 없지만, 그 객체의 속성 값은 변경할 수 있다.
var _uiState = MutableStateFlow(GameUiState())
private set

//읽기 전용으로만 접근할 수 있고, MutableStateFlow -> StateFlow로 변경하여 속성 값을 변경할 수 없다.
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

단방향 데이터 흐름(Unidirectional Data Flow, UDF) - 상태는 아래로 이동하고 이벤트는 위로 이동하는 디자인 패턴으로, 일반적으로 UI 상태를 모델로 정의하고 이렇게 정의된 객체를 뷰 모델에서 생성하여 Flow타입의 변수에 저장한다. UI 레이어는 뷰 모델 객체를 생성하고, 뷰 모델에 선언된 UI 상태 변수를 구독하면서 State타입으로 캐스팅한다. UI 요소들로 부터 야기되는 이벤트의 구현을 뷰 모델 객체를 이용하여 함수를 호출, UI 상태를 변경한다. 변경된 UI 상태는 Flow타입이므로 이를 구독한 UI 레이어에도 변경된 값이 전달된다. UI 레이어에서는 State타입으로 캐스팅한 덕분에 해당 값을 참조한 모든 구성 가능한 함수는 리컴포지션이 실행된다.

 

StateFlow<T>.collectAsState - StateFlow타입으로 부터 값을 수집하여 State타입으로 나타내는 메서드로, StateFlow타입의 값이 변경되자마자 변경된 최신의 값이 전달되고 그 값을 State로 표현하여 UI 요소에 전달하는 역할을 한다.


(Outlined)TextField의 keyboardOptions, keyboardAction 속성의 상관 관계.

OutlinedTextField(
    ...
    
    keyboardOptions = KeyboardOptions.Default.copy(
        imeAction = ImeAction.Next
    ),
    keyboardActions = KeyboardActions(
        onDone = { onKeyboardDone() }
    )
)

 

Enter 키 - 키보드 종류(keyboardOptions)를 어떤 버튼으로 설정했건 상관 없이 입력 후 엔터를 누르면 onDone이 트리거 된다.

 

keyboardOptions - 가상 키보드의 종류를 설정하는 속성으로, KeyboardOptions.imeAction을 통해 가상 키보드의 오른쪽 하단 메인 버튼의 종류를 설정할 수 있다.

 

keyboardAction - 가상 키보드 동작에 대한 콜백 함수와 트리거를 설정할 수 있는 속성으로, 가상 키보드 imeAction에서 설정한 버튼에 종속된다. 그러니까 만약 가상 키보드의 메인 버튼을 ImeAction.Next로 설정하고 콜백 함수의 트리거 동작을 onDone으로 설정한다면, 가상 키보드를 이용하여 콜백 함수를 호출할 수 없고, Enter 키를 통해서만 트리거 할 수 있게 된다.


UI 상태를 변경하는 함수

_uiState.update { currentState ->
    currentState.copy(isGuessedWordWrong = true)
}

 

MutableStateFlow<T>.update - 람다의 파라미터에 현재 value에 저장된 값이 전달되고, 람다 본문에서 반환되는 값이 value에 저장된다.

 

copy - 해당 객체의 속성을 파라미터를 통해 변경하여 새로운 객체를 생성하여 반환하는 함수.


AlertDialog - 사용자가 조치를 취해야 계속 진행할 수 있도록 하는 알림 대화상자.

AlertDialog(
    onDismissRequest = {},
    title = { Text(text = stringResource(R.string.congratulations)) },
    text = { Text(text = stringResource(R.string.you_scored, score)) },
    modifier = modifier,
    dismissButton = {
        TextButton(
            onClick = {
                activity.finish()
            }
        ) {
            Text(text = stringResource(R.string.exit))
        }
    },
    confirmButton = {
        TextButton(onClick = onPlayAgain) {
            Text(text = stringResource(R.string.play_again))
        }
    }
)

단위 테스트(Unit Test)

 

Compose BOM - Compose 라이브러리의 최신 안정화 버전에 대한 링크가 있어 BOM 버전만 업데이트 하면, 다른 Compose 라이브러리 종속 항목에 버전을 추가할 필요 없이 모든 라이브러리가 최신 버전으로 업데이트 된다.

 

implementation - 항목과 코드가 APK파일에 추가되는 종속 항목 추가 방법.

 

testImplementation - 항목과 코드가 APK파일에 추가되지 않는 종속 항목 추가 방법으로, 실행과 무관한 테스트 관련 코드들이 해당 방법으로 선언된다.

 

테스트 전략

1. 성공 경로 - 예외나 오류 조건이 없는 앱의 의도된 동작에 초점을 맞춘다.

2. 오류 경로 - 의도된 동작이 아닌 잘못된 사용자 입력에 어떻게 응답하는지 확인하는데 초점을 맞춘다.

3. 경계 사례 - 앱의 시작과 끝의 조건을 테스트하는데 초점을 맞춘다.

 

좋은 테스트 조건

1. 집중 - 범위를 좁혀 여러 코드가 아닌 개별 코드의 정확성을 검증.

2. 이해 가능 - 코드의 가독성.

3. 확정성 - 동일한 코드에 대한 동일한 결과.

4. 독립형 - 사람과 상호작용 혹은 설정 없이 개별적으로 실행.

 

테스트 함수 이름 형식

ex) thingUnderTest_TriggerOfTest_ResultOfTest

1. thingUnderTest - 테스트 대상

2. TriggerOfTest - 트리거

3. ResultOfTest - 결과

 

*각각의 테스트 메서드는 실행되기 전 해당 테스트 메서드를 포함하고 있는 클래스의 인스턴스가 생성되어 초기화된 상태로 개별적으로 실행된다.

 

*코드 적용 범위를 사용하여 테스트가 되지 않은 부분을 확인할 수 있다.

 

728x90