BigJeon Android 개발 블로그

Compose - Preview에 ViewModel 연결하기withFakeViewModel 본문

AOS - Compose

Compose - Preview에 ViewModel 연결하기withFakeViewModel

Big-Jeon 2024. 10. 28. 15:16
반응형

최근 하루 알람 이라는 개인 프로젝트를 Compose, Hilt, Room등을 이용해서 제작중에 있다.

Compose를 처음 접하고 Compose와 안드로이드 UI구성 방식과 상당히 적합하다고 생각되어,

필자는 조만간 많은 앱들이 Compose로 전환 될것이라고 생각하고 있다.

 

하지만 제작을 이어 갈수록 오히려 Compose떄문에 불편한 경우가 생겼다.

그건 바로 @Preview에서 ViewModel을 파라미터로 갖고 있는 View를 확인 하는 경우 발생 하였다.

 

필자는 Compose의 큰 장점중 하나는 UI코드를 바꾸고 해당 UI코드에 적용되어있는 변수 초기값의 변화에 따라 바로바로 UI에 적용되는

기능이 Compose의 큰 장점중 하나라고 생각한다.

해당 방식을 이용하게되면 바로바로 실제 구동 환경에서 어떻게 UI가 생기는지 확인 할 수 있고, 다르기는 하지만 JavaScript의 AJAX 기능이랑 비슷하다고 생각했다..

요즘 안드로이드 앱 중 ViewModel을 사용하지 않는 프로젝트는 없지 않은가....?.

그렇다보니 자연스럽게 특정 Screen에 해당하는 View 마다 ViewModel을 생성해서 관리 하도록 만들어 주려고 하였다.

 

하지만 이때 문제가 발생 했다.

 

<우선 내 코드를 살펴보도록 하자>

class CalenderViewScreen(private val calendarViewModel: CalendarViewModelInterface) {
    private val dateProvider = DateProvider()
    private val dateConverter = DateConverter()

    @Composable
    fun CalendarView() {
        var startX by remember { mutableFloatStateOf(0f) }
        var endX by remember { mutableFloatStateOf(0f) }
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(12.dp)
                .clip(RoundedCornerShape(15.dp))
                .background(Color.White)
                .pointerInput(Unit) {
                    detectHorizontalDragGestures(
                        onDragStart = { offset ->
                            startX = offset.x
                        },
                        onDragEnd = {
                            val dragAmount = startX - endX
                            if (kotlin.math.abs(dragAmount) >= 250) {
                                // 드래그 종료 시 방향 판단
                                if (dragAmount < 0) calendarViewModel.setBeforeMonth()
                                else calendarViewModel.setNextMonth()
                            }
                        },
                        onHorizontalDrag = { change, _ ->
                            change.consume()
                            endX = change.position.x
                        }
                    )
                },
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Column(modifier = Modifier.padding(6.dp)) {
                CalendarHeader(calendarViewModel)
                HorizontalDivider(
                    thickness = 2.dp,
                    color = Color.LightGray
                )
                CalendarDayName()
                CalendarDayList(calendarViewModel)
            }
        }
    }
}

 

 

 

해당 코드는 직접 만든 Calenadar를 보여주기 위한 Compsoe 코드이다.

코드를 살펴보면 CalendarViewScreen을 사용하기 위해서는 CalendarViewModel를 파라미터로 전달받아 사용 해야한다.

하지만 ViewModel은 Activity나 Applciation의 생명주기와 직접적인 연관이 있다보니 Preview에서 생성을 할 수 없는것이다.

 

<다음은 @Preview코드이다>

@Preview(showBackground = true)
@Composable
fun CalendarPreview() {
    EdgeToEdgeTemplate(
        navMode = NavigationMode.ThreeButton,
        cameraCutoutMode = CameraCutoutMode.Middle,
        showInsetsBorder = true,
        isStatusBarVisible = true,
        isNavigationBarVisible = true,
        isInvertedOrientation = false
    ){
        HaruAlarmTheme {
            val vm: CalendarViewModel by viewModels()
            Column(
                modifier = Modifier.background(MainColor)
            ){
                Spacer(modifier = Modifier.height(20.dp))
                CalenderViewScreen(vm).CalendarView()
            }
        }
    }
}

 

EdgeToEdgeTemplate.

ViewModel을 생성 하는 방법에는 ViewModelFactory를 사용하거나, 위 코드처럼 직접 생성을 해주는 방식을 주로 사용하게 되느데,

이때 문제가 발생 한다. @Preview는 말그대로 Preview이기 때문에 별도의 생명주기를 가지지 않고 단순히 UI구성을 보여줄 뿐이다.

그렇다보니 CalendarViewScreen을 초기화 할때 필요한 viewmodel을 생성 할 수 없고, 그 결과 Preview를 확인 하지 못한다는 말이다.

Compose....

컴포즈가 이븐하게 구성되지 않았어요....

물론 viewmodel자체를 넘기지 않고 viewModel의 값을 넘겨주는 방식을 사용 할 수도있겠지만,

그렇게 되니 Viewmodel의 값을 변경하는 코드를 작성하는게 억지로 끼워 맞추는 느낌이였고, 변수를 Remember로 지정 해주는 코드가 추가 되다보니 간결성이 저하되는 느낌이였다.

 

그렇다면 ViewModel을 두 타입으로 만들어주면 어떨까 라고 생각했다.

우리에게는 interface가 있으니 그걸 이용하는 것이다.

 

1. ViewModel 인터페이스 생성

우선 Interface를 만들어보자.

interface CalendarViewModelInterface {
    var dateConverter: DateConverter
    var dateProvider: DateProvider
    var currDate: MutableState<Calendar>
    var dayList: SnapshotStateList<com.jeon.model.dto.CalendarDate>
    fun setNextMonth()
    fun setBeforeMonth()
    fun setDayList()
}

추후 만들게 될 ViewModel은 해당 interface를 통해 동일한 형식으로 제공된다.

개인적으로 ViewModel에 대한 Interface를 만들때는 UI구성할때 보고싶은 데이터를 위주로 작성해두고 나머지 코드는 실제 ViewModel에서 따로 작성하는 방식으로 구성하는걸 선호한다.3.

 

2. FakeViewModel생성

이제 위에서 만든 ViewModel인터페이스를 통해 진짜 ViewModel이 아닌 Preview용 ViewModel을 만들어줄거다.,.

class FakeCalendarViewModel() : ViewModel(), CalendarViewModelInterface {
    override var dateConverter = DateConverter()
    override var dateProvider = DateProvider()
    override var currDate = mutableStateOf(Calendar.getInstance().apply {
        set(Calendar.DATE, 1)
    })
    override var dayList: SnapshotStateList<com.jeon.model.dto.CalendarDate> = mutableStateListOf()
    var nextTodo = TodoEvent(
        "소현이 약속",
        EventType.DAY,
        "설명 칸",
        dateConverter.dateToString(currDate.value),
        dateConverter.dateToString(currDate.value),
        true,
        30L,
        dateConverter.dateID(currDate.value)
    )


    init {
        for (i in 1 until 35){
            dayList.add(
                com.jeon.model.dto.CalendarDate(
                    Calendar.getInstance(),
                    "121212",
                    if (i == 1 || i == 7) com.jeon.model.vo.DayType.HOLIDAY else com.jeon.model.vo.DayType.WEEKDAY,
                    2,
                    "휴일입니다"
                )
            )
        }

    }

    override fun setNextMonth() {
        TODO("Not yet implemented")
    }

    override fun setBeforeMonth() {
        TODO("Not yet implemented")
    }

    override fun setDayList() {
        TODO("Not yet implemented")
    }

}

필자는 날짜마다 다음 할일과 휴일 정보 등을 보여주고 싶었다. 이를 확인해보기 위해 dayList와 nextTodo에 대한 더미데이터를 init을 통해 추가해주었다. 실제로 해당 클래스가 빌드될 일은 없으니 걱정 안해도된다.

.

 

3. 실제 사용할 ViewModel 작성

@HiltViewModel
class CalendarViewModel @Inject constructor(
    private val holidayRepository: HolidayRepository,
    private val jobDatabase: TodoEventRepository,
    private val holidayAPI: HolidayApiRepository
): ViewModel(), CalendarViewModelInterface {
    override var dateConverter = DateConverter()
    override var dateProvider = DateProvider()
    override var currDate = mutableStateOf(Calendar.getInstance().apply { set(Calendar.DATE, 1) })
    override var dayList: SnapshotStateList<com.jeon.model.dto.CalendarDate> = mutableStateListOf()
    var todoList: SnapshotStateList<TodoEvent> = mutableStateListOf()

    init {
        todoList.add(TodoEvent(
            "소현이 약속",
            EventType.DAY,
            "설명 칸",
            dateConverter.dateToString(currDate.value),
            dateConverter.dateToString(currDate.value),
            true,
            30L,
            dateConverter.dateID(currDate.value)
        ))
        viewModelScope.launch(Dispatchers.IO) {
            if (holidayRepository.getAllHolidaysCount() == 0){
                for (year in 2003 .. 2026){
                    try {
                        val response = holidayAPI.getAllHoliday(year).execute()
                        if (response.isSuccessful) {
                            val items =  response.body()?.response?.body?.items?.item
                            val holidays = ArrayList<Holiday>()
                            if (items != null) {
                                for (i in items){
                                    if (i.isHoliday == "Y"){
                                        holidays.add(Holiday(year, i.locdate, i.dateName))
                                    }
                                }
                                setDayList()
                            }
                            holidayRepository.insertAllHolidays(holidays)
                        }
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
        }
        setDayList()
    }....

아직 일단 테스트용으로 작성하였기에 API통신 하는 부분은 제외하고 Interface를 통해 구현 해야한다는것만 알아 주었으면 좋겠다...

 

해당 부분은 다른 ViewModel을 구성하는 방법과 별반 다르지 않기에 넘어가도록 하겠다.

 

4. FaveViewModel  Preview연결

이제 Preview에서 FakeViewModel을 초기화하고 연결 시켜주는 작업을 한다.

@Preview(showBackground = true)
@Composable
fun CalendarPreview() {
    EdgeToEdgeTemplate(
        navMode = NavigationMode.ThreeButton,
        cameraCutoutMode = CameraCutoutMode.Middle,
        showInsetsBorder = true,
        isStatusBarVisible = true,
        isNavigationBarVisible = true,
        isInvertedOrientation = false
    ){
        HaruAlarmTheme {
            val vm = FakeCalendarViewModel()
            Column(
                modifier = Modifier.background(MainColor)
            ){
                Spacer(modifier = Modifier.height(20.dp))
                CalenderViewScreen(vm).CalendarView()
            }
        }
    }
}

해당 방식을 통해 FakeViewModel을 구현 하게되면 내가 필요한 데이터만 추가된 viewmodel을 통해 Preview확인이 가능하다.

<다음은 실제 Preview 화면이다>

 

이상으로 Compose를 사용하여 UI를 구성할때 Preview에 ViewModel을 적용 시키는 방법이였다.

개인적으로 이부분은 추후 ViewModel의 속성값을 추가해주던지 Compose Preview에 별도 어노테이션을 추가해주던지 개선이 될 사항이라고 생각한다.

해당 부분만 해소 된다면 코드가 굉장히 간결해지고, 가독성이 더욱 증진되며, 유지보수하기 좋은 구조를 만들 수 있지 않을까 바램 아닌 바램이 있다.....

반응형