본문 바로가기

내일배움캠프 안드로이드 3기

[TIL] 24.03.15 사이드 프로젝트: Compose와 MVVM

1. 사이드 프로젝트: Compose와 MVVM

 오늘은 대부분의 시간을 Compose UI에 ViewModel을 적용하면서 보낸 것 같다. 기존 xml에서 MVVM 적용하는 건 어느정도 익숙해졌는데, Compose에 적용해보는 건 낯설었다. Hilt를 이용해 DI를 구현해둔 상태였기 때문에,  ViewModel클래스를 상속한 클래스를 만들고 @HiltViewModel 어노테이션을 붙이는 것부터 시작했다. 이후 필요한 Usecase를 주입하고 내부에 메소드를 구현하고 이런 건 xml에서와 같아서 여기까진 금방 끝났다.

 그 뒤부터는 이 문서를 참조하여 구현하기 시작했다.

 

ViewModel and State in Compose (android.com)

 

Compose의 ViewModel 및 상태  |  Android Developers

이 Codelab에서는 아키텍처 구성요소 중 하나인 ViewModel을 사용하는 방법을 알아봅니다. 구성 변경 중에 앱 상태를 유지하도록 ViewModel을 구현합니다.

developer.android.com

 

📝 LiveData와 StateFlow

 평소에 MVVM을 구현할 때 LiveData를 자주 썼는데, 이 문서에서는 StateFlow를 사용하고 있어서 나도 StateFlow를 사용하기로 했다. StateFlow와 LiveData는 아키텍처 상에서 유사한 기능을 담당하는데, 차이점도 있다. 먼저 공식 문서에서는 이렇게 안내하고 있다.

 

Android Developers

- 둘 다 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 구조로 수정도 해야하고, 아직 갈 길이 상당히 먼 것 같다.