잘 디자인된 앱 아키텍처는 향후 앱 확장과 팀 공동작업에 좋다.
가장 일반적인 아키텍처 원칙은 1. 관심사 분리, 2. 모델에서 UI 만들기이다.
관심사 분리
각각 별개의 책임이 있는 여러 클래스로 앱을 나눠야 한다는 원칙.
모델에서 UI 만들기
말 그대로 모델에서 UI를 만들어야 한다는 원칙. (지속적인 모델 권장)
모델은 데이터 처리를 담당하는 구성요소로, 앱의 뷰 객체나 앱 구성요소와 독립되어 있으므로 앱의 수명 주기 및 관련된 문제의 영향을 받지 않는다.
아키텍처의 구성요소
안드로이드의 기본 클래스 또는 구성요소는
UI 컨트롤러 (Activity/Fragment)
,ViewModel
,LiveData
,Room
이다.
UI 컨트롤러는 데이터를 display하거나 사용자 이벤트나 사용자 상호작용을 포착하여 UI를 제어한다.
✔ 데이터나 데이터 관련 의사 결정 로직은 UI 컨트롤러에 포함되어서는 안된다.
(안드로이드 시스템이 필요시 언제든 UI 컨트롤러를 제거할 수 있기 때문에 앱 데이터나 상태를 저장하면 안된다.)
→ 대신 데이터 관련 의사 결정 로직은 ViewModel
에 포함한다.
뷰에 표시되는 앱 데이터의 "모델". Activity나 Fragment가 소멸되고 다시 생성될 때 폐기되지 않는 데이터를 저장한다.
✔ 뷰 계층 구조에 액세스하거나 activity나 fragment의 참조를 보유하면 안된다.
여기에서 unscamble app의 starter 틀을 클론한다.
앱 개요
Scramble 된 단어를 맞추는 게임. 10번 진행하는데 답을 모르면 skip할 수 있고, 틀리면 다시 입력할 수 있다. 게임이 끝나면 점수와 함께 Exit와 Play Again 버튼이 뜬다.
ViewModel을 사용하려면 먼저 라이브러리 종속 항목을 추가한다. ( latest version 확인 )
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
GameFragment
의 상단에 다음과 같이 GameViewModel
유형의 속성을 추가한다.
private val viewModel: GameViewModel by viewModels()
여기서 by viewModels()
를 속성 위임이라고 한다.
Kotlin 속성 위임을 사용하면 getter-setter 책임을 다른 클래스에 넘길 수 있다.
대리자 클래스는 by
로 정의할 수 있고, 대리자 클래스는 속성의 getter, setter 함수를 제공하고 변경사항을 처리한다.
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
속성 위임을 하지 않고, private val viewModel = GameViewModel()
이런식으로 뷰 모델을 초기화하는 경우 기기를 회전하는 등의 기기에서 구성이 변경될 때 앱이 viewModel
참조 상태를 손실하게 된다.
속성 위임을 통해서 viewModel
객체의 칙임을 viewModels
라는 별도의 클래스에 위임한다. 대리자 클래스는 첫 번째 액세스 시 자동으로 viewModel
객체를 만들고, 이 값을 유지하다 요청이 있을 때 반환한다.
GameViewModel
을 추가해 GameFragment
에 있는 데이터와 관련된 내용을 옮긴다.
그림처럼 MainActivity
에 GameFragment
가 포함되어 있고, GameFragment
는 GameViewModel
에 있는 게임 관련 정보에 액세스한다.
class GameViewModel : ViewModel() {
}
GameViewModel
은 추상클래스 ViewModel
을 확장한다.
ViewModel
내에서는 데이터를 수정할 수 있어야 하므로 데이터는 private 및 var다. 하지만 외부에서는 데이터를 읽을 수 있지만 수정할 수는 없어야 한다. 이때 backing property를 사용한다.
private var _count = 0
val count: Int get() = _count
private var
인 _count
는 내부에서 수정할 수 있지만 외부에서 접근할 수 없고, public val
인 count
는 외부에서 읽을 수만 있다.
❗ ViewModel 내부의 변경 가능한 데이터는 항상 private이어야 한다!
GameViewModel
에 currentScrambledWord
를 backing property를 사용해 선언한다.
private var _currentScrambledWord = "test"
val currentScrambledWord: String get() = _currentScrambledWord
GameFragment
에서 currentScrambledWord
데이터값을 사용하려면 viewModel.currentScrambledWord
형태로 사용할 수 있다.
프레임워크는 activity이나 fragment의 범위가 유지되는 동안 ViewModel을 유지한다.
GameViewModel
과 GameFragment
에 Log를 추가해 동작을 확인할 수 있다.
앱을 실행하면 GameFragment
와 GameViewModel
이 생성된다.
기기 화면을 회전하면 GameFragment
는 매번 소멸되고 다시 생성되지만 GameViewModel
은 한 번만 생성되고 기기 회전으로 소멸되지 않는다.
앱에서 나가면 GameViewModel
이 소멸되고 콜백 onCleared()
가 호출되고, GameFragment
가 소멸된다.
Material design 구성요소 라이브러리인 MaterialAlertDialog
를 사용해 대화상자를 띄울 수 있다.
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
setNegativeButton(getString(R.string.exit)) { _, _ -> exitGame() }
이런 구문은 후행 람다 구문이라고 한다...?