Android 아키텍쳐는 UI Controller, ViewModel
, LiveData
, Room
으로 이루어져 있다. LiveData
과 Room
은 이후에 알아보고, 일단은 UI Controller와 ViewModel
에 대해서 먼저 알아보자.
UI Controller
액태비티와 프래그먼트를 말한다. UI 컨트롤러는 화면에 뷰를 그리고, 사용자와 상호작용하는 모든 UI를 제어한다.
🔎 UI Controller는 뷰와 데이터를 화면에 그리고 사용자 이벤트에 응답한다.
ViewModel
앱의 데이터나 데이터와 관련된 로직은 UI 컨트롤러에 포함되어서는 안된다. 이것들은 ViewModel
에 포함되어야 한다. ViewModel
은 뷰에 표시되는 앱 데이터의 모델이다. 액티비티/프래그먼트가 소멸될때 폐기되면 안되는 데이터를 저장한다.
🔎 ViewModel은 UI에 필요한 모든 데이터를 보유하고 처리한다. view binding 객체 등에 엑세스해서는 안된다.
예제에서 ViewModel은 다음과 같은 구조로 앱에 추가된다.
MainActivity
에 GameFragment
가 포함되어 있으며, GameFragment
는 GameViewModel
에 있는 게임 관련 정보에 액세스한다.
private val viewModel = GameViewModel()
이런식으로 초기화하면 기기를 회전하는 등 앱의 구성이 변경되면 viewModel
참조의 상태를 잃어버리게 된다.
//대리자 속성은 다음과 같이 by 절 및 대리자 클래스 인스턴스를 사용하여 정의한다.
private val viewModel: GameViewModel by viewModels()
대신 속성 위임 접근 방식을 사용해 viewModel
객체의 책임을 viewModels
라는 별도의 클래스에 위임하자. (즉, viewModel
객체에 액세스하면 이 객체는 대리자 클래스 viewModels
에 의해 내부적으로 처리된다.)대리자 클래스는 첫 번째 액세스 시 자동으로 viewModel
객체를 만든다. 얘는 기기 회전 등 구성 변경이 있어도 값을 유지하며, 요청이 있을때 값을 반환한다.
ViewModel의 수명 주기
ViewModel
은 화면 회전 등의 구성 변경으로 인해 소멸되는 경우에도 없어지지 않는다. 사용자의 새 activity 인스턴스는 기존 ViewModel
인스턴스에 다시 연결된다. 앱을 종료하면 그제서야 onCleared()
가 호출돼 ViewModel
이 삭제된다.
대화상자
1. 알림 대화상자
2. 제목(선택사항)
3. 메시지
4. 텍스트 버튼
Android는 다양한 유형의 대화상자를 제공한다. MaterialAlertDialog
를 사용하여 머티리얼 가이드라인을 따르는 대화상자를 사용할 수 있다.
//대화상자 만드는 함수를 만들어서 필요할때 호출하자.
private fun showFinalScoreDialog() {
//requireContext는 프래그먼트에서 context 반환할때 쓰는 함수.
MaterialAlertDialogBuilder(requireContext())
}
대화상자 함수를 정의해서 필요할때 적절히 쓰도록 해보자.
private fun showFinalScoreDialog() {
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() //알림상자를 표시
}
MaterialAlertDialog
에 각종 옵션들을 붙여준다. 마지막으로.show()
를 붙여서 알림상자를 표시한다.
LiveData
는 수명 주기를 인식하는 데이터 홀더 클래스이다.
LiveData
의 특징들
LiveData
객체에서 보유한 데이터가 변경되면 관찰자가 알 수 있음.)LiveData
는 수명 주기를 인식한다. LiveData
는 활성 수명 주기인 활동/프레그먼트의 관찰자만 업데이트한다.🔎 예제의 앱에서 updateNextWordOnScreen()
메서드는 아주 많은 위치에서 호출된다. Livedata
를 사용하면 UI를 업데이트하기 위해 여러 위치에서 이 메서드를 호출하지 않아도 된다. 관찰자에서 한 번만 호출한다.
MutableLiveData
MutableLiveData
내부에 저장된 값을 변경할 수 있는LiveData
이다.
private val _currentScrambledWord = MutableLiveData<String>()
val currentScrambledWord: LiveData<String>
get() = _currentScrambledWord
...
private fun getNextWord() {
...
} else {
_currentScrambledWord.value = String(tempWord)
...
}
}
LiveData
의 값에 접근하기 위해서는 .value
를 사용한다.
//관찰자 추가
//첫번째 매개변수 viewLifecycleOwner. 이를 통해 STARTED 또는 RESUMED일때만 관찰자에 알림.
//두번째 매개변수 newWord (뒤섞인 글자값을 저장. 람다표현식. neworld를 매개변수로 받아 오른쪽 식을 반환)
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
{ newWord ->binding.textViewUnscrambledWord.text = newWord })
예제에서는 프레그먼트에 관찰자를 추가한다.onViewCreated
함수 안에 위와 같이 observe()
를 사용한 코드를 작성하면 된다.
currentScrambledWord
외의 변수도 LiveData
타입으로 선언하고 .value
를 통해 접근하고 observe()
를 통해 관찰자를 붙여줄 수 있다.
이전에 배운 뷰결합을 사용하면, 코드에서 뷰를 참조할 수 있었다. 하지만 뷰에서 앱 데이터를 참조할 수는 없었다. 이 작업에는 데이터 결합을 사용한다. 데이터 결합 라이브러리는 Android Jetpack 라이브러리의 일부이다.
android:text="@{gameViewModel.currentScrambledWord}"
레이아웃 파일에서 데이터결합의 사용 예. @{}
구문을 사용한다.
사용하기위해서는 build.gradle
파일에서
buildFeatures {
dataBinding = true
}
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
이렇게 수정해준다.
이후 xml파일에 가서 루트 요소(여기선 ScrollView)에 마우스 오른쪽-show context action을 누르면
data binding layout으로 바꾸는 옵션이 떠서 간편하게 바꿀 수 있다.
binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
데이터 결합을 사용하도록 binding
변수를 선언하는 부분도 바꿔준다.
xml파일이 data binding layout으로 바뀌면 <data></data>
태그가 생긴다.
<data>
<variable
name="gameViewModel"
type="com.example.android.unscramble.ui.game.GameViewModel" />
</data>
<data>
태그 내에 <variable>
이라는 하위 태그를 추가하고 GameViewModel
유형의 gameViewModel
이라는 속성을 선언한다. 이 속성을 사용하여 ViewModel
의 데이터를 레이아웃에 결합할 수 있다.
<data>
<variable
name="gameViewModel"
type="com.example.android.unscramble.ui.game.GameViewModel" />
<variable
name="maxNoOfWords"
type="int" />
</data>
평범한 int 변수등도 줄줄이 추가할 수 있다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.gameViewModel = viewModel
binding.maxNoOfWords = MAX_NO_OF_WORDS
//레이아웃에 수명주기 전달→ 변수가 바뀌면 ui요소가 알아서 업뎃됨
binding.lifecycleOwner = viewLifecycleOwner
...
}
레이아웃 파일에서 선언해준 애들을 onViewCreated
에서 새롭게 초기화하자.
이제 결합표현식 @{}
를 사용해서 ViewModel
에 있는 변수가 변경되면 그에따라 뷰를 업데이트 할 수 있게 해보자.
<TextView
android:id="@+id/textView_unscrambled_word"
...
android:text="@{gameViewModel.currentScrambledWord}"
.../>
이제 LiveData
의 관찰자가 변경을 감지하면 text를 업데이트 하는 코드는 필요가 없다.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
{ newWord ->
binding.textViewUnscrambledWord.text = newWord
})
이런 코드들 삭제해준다.
결합 표현식에 @string을 껴넣을 수도 있다.
//layout xml
android:text="@{@string/example_resource(user.lastName)}"
//string.xml
<string name="example_resource">Last Name: %s</string>
이런식으로!!