1. 사이드 프로젝트: Compose와 MVVM
오늘은 대부분의 시간을 Compose UI에 ViewModel을 적용하면서 보낸 것 같다. 기존 xml에서 MVVM 적용하는 건 어느정도 익숙해졌는데, Compose에 적용해보는 건 낯설었다. Hilt를 이용해 DI를 구현해둔 상태였기 때문에, ViewModel클래스를 상속한 클래스를 만들고 @HiltViewModel 어노테이션을 붙이는 것부터 시작했다. 이후 필요한 Usecase를 주입하고 내부에 메소드를 구현하고 이런 건 xml에서와 같아서 여기까진 금방 끝났다.
그 뒤부터는 이 문서를 참조하여 구현하기 시작했다.
ViewModel and State in Compose (android.com)
📝 LiveData와 StateFlow
평소에 MVVM을 구현할 때 LiveData를 자주 썼는데, 이 문서에서는 StateFlow를 사용하고 있어서 나도 StateFlow를 사용하기로 했다. StateFlow와 LiveData는 아키텍처 상에서 유사한 기능을 담당하는데, 차이점도 있다. 먼저 공식 문서에서는 이렇게 안내하고 있다.
- 둘 다 observable한 데이터 홀더 클래스이고, 아키텍처 내에서 비슷한 패턴을 따른다.
- StateFlow는 생성자를 통해 초기 상태를 지정해야하지만, LiveData는 그렇지 않다.
- LiveData.observe()는 뷰가 STOPPED 상태가 됐을 때 소비자가 자동으로 해제되며, 반면에 StateFlow에서 collect 되는 것은 자동으로 중지되지 않는다. (LiveData는 안드로이드 뷰 생명주기에 영향을 받는다는 소리)
그 밖에도 차이가있다면 StateFlow는 코틀린의 Flow API 이며, 따라서 안드로이드에 귀속적이지 않다는 것이 있다. Domain영역 등에서도 사용할 수 있다는 소리이다. LiveData가 변경 가능하고 Observer 패턴에 의해 관찰되는 객체라면, StateFlow는 변경 가능하고 collect 될 수 있는 상태 Flow라고 이해하기로 했다. Compose는 지극히 상태에 의존하기 때문에 어떻게 보면, Compose로 UI를 사용할 때 StateFlow를 사용하는 것은 당연한 것 같기도 하다. 제공되는 remember API는 State를 강력히 지원한다.
@HiltViewModel
class SearchViewModel @Inject constructor(
private val loadImageTestUseCase: LoadImageTestUseCase
): ViewModel() {
private val _mediaListState = MutableStateFlow<List<MediaItem>>(emptyList())
val mediaListState: StateFlow<List<MediaItem>> = _mediaListState.asStateFlow()
fun searchMedia(query: String) {
viewModelScope.launch(Dispatchers.IO) {
val mediaList = loadImageTestUseCase(query)
_mediaListState.emit(mediaList)
}
}
}
결과적으로 일단 뷰 모델은 저런식으로 만들게 되었다. 물론 당장 테스트 해보기 위한 최소한의 구조이다. 미세하게 LiveData와는 용법에 있어서 차이를 보이고 있다.
@Composable
fun SearchScreen(searchViewModel: SearchViewModel) {
val mediaListState by searchViewModel.mediaListState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
) {
MediaSearchBar(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 8.dp)
) { searchViewModel.searchMedia("강아지") }
LazyColumn(
content = {
items(mediaListState.size) {
ColumnItemCard(mediaListState[it].info)
}
},
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
)
}
}
그리고 뷰 모델의 StateFlow를 이용해 데이터를 뿌려줄 뷰 같은 경우에는, 일단 위와 같이 구현해 테스트했다. MediaSearchBar의 요소 중 하나를 클릭 시 인자로 받은 함수를 실행하도록 간단하게 설정해두고, 주입된 뷰 모델의 StateFlow를 collect하며, LazyColumn의 내용물에 일부 반영해 뿌려주게 했다.
State Hoisting 등을 디테일하게 따져 만든 것은 아니고, 테스트를 위해 간단하게 만들어 본 것이라 나중에 다 뜯어고쳐야 할 것 같긴하다..
다행히 의도대로 동작하고, 앱도 죽지 않는 것으로 보아 로직의 큰 흐름은 틀리지 않은 것 같다. 다음에는 디테일한 부분을 고려해서 수정해 나가야 할 것 같다.
비즈니스 로직에서 쓰이는 데이터를 wrapping해서 예외 발생 등도 Data 레이어에서 Presentation 레이어로 전달될 수 있게 해야하고, Room을 이용해 좋아요 한 것들을 로컬에 저장하는 기능과 관련 보일러 플레이트 코드들도 작성해야하고, 이를 리스트에 뿌려주는 데이터에도 반영해야 한다. 저 뿌려주는 아이템들도 제목만이 아니라 모든 내용을 제대로 보여줄 수 있게 바꿔야하고, 완성도 높은 Compose 구조로 수정도 해야하고, 아직 갈 길이 상당히 먼 것 같다.
'내일배움캠프 안드로이드 3기' 카테고리의 다른 글
[TIL] 24.03.19 개인 과제 (0) | 2024.03.19 |
---|---|
[TIL] 24.03.18 Android 앱개발 입문 주차 (0) | 2024.03.18 |
[TIL] 24.03.14 사이드 프로젝트: Hilt 적용하기 (2) | 2024.03.14 |
[TIL] 24.03.13 3주차 개인 과제 (1) | 2024.03.13 |
[TIL] 24.03.11 사이드 프로젝트, 3주차 개인과제 (0) | 2024.03.11 |