Preview_심폐소생술 - boostcampwm-2024/and04-Nature-Album GitHub Wiki
✅ 진행한 기능
- Preview 안 보이는 버그 수정
- Home ✅
- Album ✅
- AlbumFolder ✅
- LabelSearch ✅
- MyPage ✅
- PhotoInfo ✅
- SavePhoto ✅
💡더미 데이터를 받아 사용할 수 있는 하위 컴포저블 만들기!
우선 ViewModel 사용으로 인해 Preview가 보이지 않는 문제를 해결하고자 했다.
Github Compose-Samples | Jetsnack | Cart.kt
compose-samples에서 사용한 방식을 분석하였고, 여기서 정말 많은 아이디어를 얻었다.
사용한 방법을 간단하게 설명하면 다음과 같다.
- viewmodel 인스턴스를 생성한 뷰모델 컴포저블을 생성한다.
- 해당 뷰모델에서 필요한 상태와 값을 전달받는 하위 컴포저블을 생성한다.
- Preview에서는 2번 과정에서 생성한 컴포저블을 연결한다.
아래는 프로젝트 진행 중 수정한 과정이다.
현재 Preview가 작동하지 않는 SavePhotoScreen 을 해결하기 위하여 똑같은 이름을 가진 SavePhotoScreen 컴포저블을 만들었고 이 컴포저블에 필요한 값들을 전달하는 방식이다.
@Composable
fun SavePhotoScreen(
location: Location?,
model: Uri,
onBack: () -> Unit,
onSave: () -> Unit,
onLabelSelect: () -> Unit,
description: String = "",
label: Label? = null,
onNavigateToMyPage: () -> Unit,
viewModel: SavePhotoViewModel = hiltViewModel(),
) {
SavePhotoScreen( // 이 부분의 컴포저블을 Preview로 사용
model = model,
label = label,
location = location,
photoSaveState = photoSaveState,
rememberDescription = rememberDescription,
onDescriptionChange = { newDescription -> rememberDescription.value = newDescription },
isRepresented = isRepresented,
onRepresentedChange = { isRepresented.value = !isRepresented.value },
onNavigateToMyPage = onNavigateToMyPage,
onLabelSelect = onLabelSelect,
onBack = onBack,
savePhoto = viewModel::savePhoto
)
}
즉, 똑같은 이름의 컴포저블을 2개 만들었고 viewmodel와 상태들을 넘겨받는 하위 SavePhotoScreen을 구현해 이 컴포저블을 Preview로 연결하여 이 문제를 해결했다.
더 간단한 코드의 예시이다.
@Composable
fun A(viewModel: MainViewModel: viewModel()){
val value by myPageViewModel.value.collectAsStateWithLifecycle()
B(value, viewModel::postValue)
}
@Composable
fun B(value: String, postValue: () -> Unit){
Button(onClick = postValue){
Text(text = value)
}
}
@Preview
fun Preview(){
MaterialTheme{
B("dummy Data", { })
}
}
최상의 컴포저블인 A에서 뷰모델 인스턴스를 생성한다. 이 A 컴포저블에서는 UI를 그리지 않고 상태와 필요한 값들을 그대로 B 컴포저블에 전달한다.
B 컴포저블은 A 컴포저블에게 받은 상태와 값을 통해 UI를 그린다.
그렇게되면 B 컴포저블 파라미터에 더미데이터를 넣어준다면 Preview가 정상적으로 작동하기 시작한다.
❓왜 뷰모델의 상태와 함수를 파라미터로 전달해야 할까?
Android Developers Compose 및 기타 라이브러리
우선 우리 팀에서 작성한 코드 중 하위 컴포저블에서 viewmodel 인스턴스를 사용하는 부분이 정말 많았다.
이렇게 viewmodel 인스턴스를 마구자비로 여러 곳에서 생성하면 안된다는 것을 공식문서를 통해 찾을 수 있었다.
이 가이드라인은 ViewModel
인스턴스를 여러 컴포저블에서 생성하는 것을 피하라는 의미이다.
즉, MyScreen
과 MyScreen2
에서 각각 viewModel()
을 호출하여 새로운 ViewModel
인스턴스를 생성하는 것이 아니라, 같은 ViewModel
을 공유하는 것이 중요하다는 뜻이다.
우리가 구현한 컴포저블 중에 상위 컴포저블과 하위 컴포저블 둘 다 뷰모델을 생성하는 코드가 존재해 전부 제거하고, 상위 컴포저블의 상태나 함수를 전달하는 식으로 전면 수정했다.
ViewModel
은 특정 화면(액티비티나 내비게이션 그래프의 스코프) 내에서만 하나의 인스턴스가 생성되어야 한다.
이를 통해 ViewModel
이 해당 화면의 수명 주기와 맞춰 관리되고, 여러 컴포저블 간에 상태가 공유될 수 있다.
따라서, MyScreen
과 MyScreen2
가 같은 ViewModel
을 공유해야 한다면, 두 컴포저블 모두에서 viewModel()
을 호출하는 대신, 상위 컴포저블에서 ViewModel
을 한 번만 호출하고, 필요한 데이터나 함수만 하위 컴포저블로 전달하는 것이 좋다.
이렇게 하면 코드의 재사용성이 높아지고, 테스트도 쉬워지며, 예기치 않은 ViewModel
인스턴스의 중복 생성도 방지할 수 있다.
💡 즉, viewmodel 인스턴스는 상위 컴포저블에서 생성하고 필요한 값들은 파라미터로 넘겨주는 방식으로 구현해야하는 것이다.
넘어와서 viewmodel과 여러 상태 관련해서는 상위 컴포저블에 위치시키고 UI와 관련된 부분만 따로 뺀 컴포저블을 만들어 Preview로 사용한다는 것이 아이디어이다.
이런식으로 구현한다면 Preview에선 더미데이터만 넣어준다면 다시 정상적으로 동작하는 것을 확인할 수 있다!
또 Compose-samples를 참고하면서 아이디어를 하나 더 얻었다.
기존에 상태 관리를 할 때, = remember
을 사용해 하위 컴포저블에게 State<타입>
을 넘겨주고 .value
를 통해 실제 값을 사용했었다.
하지만 by
키워드를 사용하면 상태 값을 직접 접근할 수 있게 된다. 즉 .value
를 사용하지 않고 실제 값을 바로 사용할 수 있게 되는 것이다.
또한 by
키워드를 사용하고, State<T>
를 파라미터 타입으로 넘겨주는 것보다 실제 값 자체를 넘겨주는 것 자체가 Preview에서의 사용이 간편하다는 것을 알게되었다.
실제로 =
키워드를 사용하여 State<T>
를 파라미터로 넘겨주게 된다면 State
는 인터페이스라 초기화 할 수 없다는 에러메세지를 띄운다.
그렇다면 다른 방식으로mutableStateOf()
를 통해 더미 데이터를 넣어주려고 하니 이번에는 remember 키워드랑 같이 써야한다는 에러메세지를 만날 수 있었다.
즉, State<T>
타입을 더미데이터로 만들기 위해서는 Preview에서도 아래와 같은 코드를 작성하여 직접 상태를 만들고 더미데이터로 넣어줘야하는 번거로움이 있었다.
val uiState = rememberSaveable { mutableStateOf(UiState.Success) }
실제 Compose-samples에서는 상위 컴포저블에서 상태 관리는 by
키워드를 사용하여 위임하고 있었다.
그렇게 되면 하위 컴포저블로 넘겨주는 파라미터로 State<T>
가 아닌, 실제 값을 넣어주고 있기에 Preview에서도 간편하게 더미데이터를 생성하여 사용하고 있었다.
=
키워드와 by
키워드의 선언은 동일한 것이라는 것을 공식 문서에서 직접 확인했기에 이 정도면 충분한 근거라고 생각하여 Compose-samples의 아이디어를 바탕으로 우리의 Preview 문제를 해결했다.
실제로 팀원분께서 이런 부분에 대해 PR에 질문을 주셨고 이 근거를 바탕으로 답변을 남겼다.