<이전글>
<코드랩>
# 시작하며
이번 코드랩에서는 평점기능 추가를 위한 프래그먼트 연결, 버튼 상태관리, 스낵바띄우기를 배웠다. LiveData를 활용해서 비슷한 방법을 사용해서 기능을 구현할 수 있었다.
이번 코드랩에서는 이상하게 에러가 너무 많이생겨서 에러를 해결하는데 시간을 많이 잡아먹었다.
# 평점 기능 연결하기1
START버튼 -> STOP을 누르면 수면 평점을 클릭하여 저장하는 기능을 구현해야 한다.
먼저 START를 누르면 만들어지는 tonight에 대해서 STOP을 누를시 그 객체 자체를 넘겨줘야한다. 그 객체에서 nightId값을 통해서 디비에 선택한 수면퀄리티를 update해야하기 때문이다.
SleepTrackerViewModel에 아래를 추가해주자.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
fun onStopTracking() {
viewModelScope.launch {
val oldNight = tonight.value ?: return@launch
oldNight.endTimeMilli = System.currentTimeMillis()
update(oldNight)
//추가
_navigateToSleepQuality.value = oldNight
}
}
이렇게 함으로써, STOP버튼을 클릭시, _navigateToSleepQuality라이브데이터가 변화됨을 UI컨트롤러에서 옵저빙하다가 _navigateToSleepQuality가 바뀜을 알고 퀄리티프래그먼트를 연결시켜준다. 여기서 night.nightID를 인자로 넘겨주어 어떤 에티티에 update해야하는지 가지 알려준다.
SleepTrackerFragement에 아래를 추가해주자.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer { night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
Log.d("mydebug", "--@@--")
sleepTrackerViewModel.doneNavigating()
}
})
this와 SleepTrackerFragmentDirections에 빨간줄이 뜨지만 실행은 잘 된다. 이거때문에 꽤 시간을 잡아먹었는데 결국 빨간줄은 없애지는 못하고, 빌드는 잘 됨을 확인 했다.
sleepTrackerViewModel.doneNavigating()을 호출해줌으로써 _navigateToSleepQuality을 null로 만들어주어 재호출되지 않도록 한다.
# 평점 기능 연결하기2 - 저장
이제 넘겨진 퀄리티프래그먼트에서 평점을 누르면 DB에 값이 update되고, 홈화면으로 넘어오도록 해줘야한다. SleepQualityViewModel을 다음과 같이 만들어 줘야 한다. 뷰모델은 팩토리로 만들어지는 것에 유념하자.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao
) : ViewModel() {
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker : LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
fun onSetSleepQuality(quality: Int) {
viewModelScope.launch {
val tonight = database.get(sleepNightKey) ?: return@launch
tonight.sleepQuality = quality
database.update(tonight)
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
}
여기서 _navigateToSleepTracker을 통해서 평점이 클릭됨을 옵저버를 통해 감지하고 홈 프래그먼트로 전달시키는 역할을 한다.
onSetSleepQuality은 onClick리스너로 묶어서 평점이 클릭되면 실행되는 함수이다. 즉 평점 클릭 -> onSetSleepQuality호출 ( db에 값 반영) -> _navigateToSleepTracker을 통해 홈프래그먼트로 넘겨주기의 작업으로 진행된다.
뷰모델생성을 위한 뷰모델 팩토리를 아래와 같이 만들어준다.
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
사실 잘 이해는 가지 않지만 보일러플레이트형식이라고 코드랩에서 소개한다. 최근 GOF를 공부하기위해서 책을 열심히 읽고 있는 중이다. 언젠가는 저런식으로 만드는 이유를 이해할 수 있을 것이다.
이제 뷰모델을 생성하는 과정을 거친 후, 위에서 말한 옵저버를 연결하여 홈화면으로 넘어가기 위해서 QualityFragement를 만들어주자.
class SleepQualityFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Get a reference to the binding object and inflate the fragment views.
val binding: FragmentSleepQualityBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_sleep_quality, container, false)
val application = requireNotNull(this.activity).application
val arguments = SleepQualityFragmentArgs.fromBundle(requireArguments())
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
val sleepQualityViewModel = ViewModelProvider(this,viewModelFactory).get(SleepQualityViewModel::class.java)
binding.sleepQualityViewModel = sleepQualityViewModel
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
Log.d("mydebug","--@@--")
sleepQualityViewModel.doneNavigating()
}
})
return binding.root
}
}
여기도 SleepQualityFragmentArgs,SleepQualityFragmentDirections 에도 빨간 에러가 뜨지만 앱빌드는 성공적으로 이루어진다. 왜 그런지 모르겠다..
xml의 각 이미지에 databindng을 통해 onClick리스너를 연결해주도록 하자.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
위와 같이 0~5까지 하나씩 입력해준다.
# 버튼 활성/비활성
SleepTrackerViewModel에 버튼 활성/비활성을 위한 변수를 만들어 준 후, xml에 데이터바인딩을 통해 연결해주자.
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
버튼의 enabled속성은 true/false값을 사용한다.
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
# 스낵바 만들기
토스트 메시지와 비슷한 스낵바를 만들어 주도록 한다. 스낵바가 나오는 타이밍 또한 LiveData를 통해서 관리할 수 있다. 맨 처음 했던거와 비슷한 방식이다. onClear가 눌리면 스낵바가 나오도록 해주자.
onClear 버튼이 눌리면 만들어준 라이브데이터 변수가 true로 바뀌고, UI컨트롤러에서 옵저빙해서 스낵바를 띄우도록 한다.
SleepTrackerViewModel에 아래와 같이 추가해주자.
// 상태 관리를 위한 라이브데이터
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
// CLEAR버튼 클릭시 호출되는 메서드
fun onClear() {
viewModelScope.launch {
clear()
tonight.value = null
// 추가
_showSnackbarEvent.value = true
}
}
이제 UI컨트롤러(프래그먼트)에서 옵저빙을 통해 스낵바를 띄우기만 하면 된다.
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer {
if (it == true) { // Observed state is true.
Snackbar.make(
requireActivity().findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
})
# 마치며
이번 코드랩을 통해서 ROOM-DB사용법, 코루틴을 통한 DB 호출, 프래그먼트 전환시 인자 넘겨주기, 스낵바, 라이브데이터 다루기 등을 학습할 수 있었다. 깊은 이해는 아니지만, 아키텍처와 ROOM을 결합하여 이러한 플로우로 사용됨을 알 수 있었다. 다음 시간에는 안드로이드의 꽃이라고 불릴 수 있는 리사이클러뷰에 대해서 공부할 것 같다. 지금까지 리사이클러뷰를 많이 써오긴 했지만, 구글에서 제시하는 방법은 어떤 것일지 궁금하기도 하다.
'•App > 안드로이드 with Kotlin' 카테고리의 다른 글
[안드로이드 코드랩/리사이클러뷰-1] 리사이클러 뷰 개념, 뷰 홀더 개념, 리사이클러뷰 만들기 (0) | 2022.09.01 |
---|---|
[안드로이드 코드랩/Room-2] 코루틴을 이용해 Room CRUD구현해보기, LiveData를 반환하는 DAO (0) | 2022.08.21 |
[안드로이드 코드랩/Room-1] 안드로이드 Room-DB만들기, Entity개념, DAO (1) | 2022.08.21 |
[안드로이드 코드랩/아키텍처-4] LiveData를 transformations사용하여 변환하기, 안드로이드 타이머 구현하기 (0) | 2022.08.20 |
[안드로이드 코드랩/아키텍처-3] 데이터바인딩(data-binding) 을통한 뷰모델(viewModel), 라이브데이터(liveData) 결합 (0) | 2022.08.19 |