[나o만] MVI 패턴 적용하기

love&peace·2024년 11월 18일

나o만

목록 보기
1/2
post-thumbnail

What is MVI?!

모델(Model), 뷰(View), 인텐트(Intent)의 세 가지 구성 요소로 이루어져 있습니다.

  • Model : 상태 처리 로직, 비지니스 로직
  • View : 상태를 바탕으로 사용자 인터페이스 구성, intent를 model로 전달
  • Intent : 사용자의 행동(+ 시스템 이벤트) - 상태 변화 트리거

MVI 패턴의 핵심은 단방향 데이터 흐름(Unidirectional Data Flow)

  • 사용자의 입력(Intent)이 Model에 전달되어 상태를 변경하고, 변경된 상태는 View를 통해 사용자에게 반영됩니다.

    나o만 아키텍쳐 일부

사용한 이유?

  • MVVM과 차이 : 상태를 하나로만 관리 -> 여러개의 프로퍼티(liveData, flow)로 관리하지 않아서 가독성 증가, 코드량 감소
  • jetpack Compose 사용 :
    1.상태 관리 중요성 증가
    2.상태를 하나로 관리해서 상태 추적과 업데이트 유리
    3.단반향 데이터 흐름이 compose의 (event -> 상태 변경 -> ui 업데이트)와 일치

key point

  • State : 여러 상태를 하나로 관리 (view 업데이트에 사용)
  • Event : 사용자의 행동을 받아 model로 보냄 (viewModel에서 비지니스 로직, 상태 처리 로직)
  • SideEffect : 사용자에게 보여줘야 하는 효과 (view에서 효과 ex-Dialog 보여주기, 화면이동)

예시 :

class VoteMainContract {

    data class VoteMainViewState(
        val loadState: LoadState = LoadState.SUCCESS,
        val groupId: Long = 0L,
        val voteList: List<AgendaDetailInfoModel> = listOf(),
        val voteDetail: AgendaInfoListModel? =null,
        val groupName: String = "",
        val groupNameList : List<ShareGroupNameInfoModel> = listOf()
    ) : ViewState

    sealed class VoteMainSideEffect : ViewSideEffect {
        object NaviAgendaAdd :VoteMainSideEffect()
        object NaviBack : VoteMainSideEffect()
        data class NaviVoteDetail(val agendaId: Long) : VoteMainSideEffect()
    }

    sealed class VoteMainEvent : ViewEvent {
        object InitVoteMainScreen : VoteMainEvent()
        object onAddAgendaInBoxClicked : VoteMainEvent()
        object OnBackClicked :VoteMainEvent()
        object OnPagingVoteList : VoteMainEvent()
        data class OnAgendaItemClicked(val agendaId : Long) :VoteMainEvent()
        data class OnClickDropBoxItem(val member: ShareGroupNameInfoModel) : VoteMainEvent()
    }
}

view에서

  • event 보내기
  • 상태에 따라 ui 변환
  • sideEffect 분기 처리

VoteMainScreen.kt

@Composable
fun VoteMainScreen(
    //**//
) {
	//state 받기
    val viewState by viewModel.viewState.collectAsState()
  	//**//
	
    //sideEffect 받기
    LaunchedEffect(key1 = viewModel.effect) {
        viewModel.effect.collect { effect ->
            when (effect) {
                VoteMainContract.VoteMainSideEffect.NaviAgendaAdd -> {
                    navigationAgenda(viewState.groupId)
                }

                VoteMainContract.VoteMainSideEffect.NaviBack -> {
                    navigationBack()
                }

                is VoteMainContract.VoteMainSideEffect.NaviVoteDetail -> {
                    navigationVoteDetail(effect.agendaId)
                }
            }
        }
    }
	
    //state 데이토로 ui 분기
    when (viewState.loadState) {
        LoadState.LOADING -> {
            StateLoadingScreen()
        }
        
    //..//
    
    	viewState.groupNameList.forEachIndexed { _, member ->
                                    androidx.compose.material3.DropdownMenuItem(
                                        text = {
                                            Text(
                                                member.name,
                                                fontSize = 16.sp,
                                                modifier = Modifier.fillMaxWidth()
                                            )
                                        },
                                        
                                        //event 보내기
                                        onClick = {
                                            viewModel.setEvent(
                                                VoteMainContract.VoteMainEvent.OnClickDropBoxItem(
                                                    member = member
                                                )
                                            )
     //..//

viewModel에서

  • event 분기 처리
  • 비지니스 로직
  • 상태 업데이트
@HiltViewModel
class VoteMainViewModel @Inject constructor(
    private val agendaInfoListUsecase: AgendaInfoListUsecase,
    private val savedStateHandle: SavedStateHandle,
    private val checkSpecificGroupUsecase: CheckSpecificGroupUsecase,
    private val shareGroupNameListUsecase: ShareGroupNameListUsecase
) : BaseViewModel<VoteMainContract.VoteMainViewState, VoteMainContract.VoteMainSideEffect, VoteMainContract.VoteMainEvent>(
    VoteMainContract.VoteMainViewState()
) {
	//..//

    init {
        updateState { copy(groupId = savedStateHandle[KEY_GROUP_ID] ?: 0L) }
        fetchGroupNameList()
    }
    
	//event 분기 처리
    override fun handleEvents(event: VoteMainContract.VoteMainEvent) {
        when (event) {
            is VoteMainContract.VoteMainEvent.InitVoteMainScreen -> {
            	//비지니스 로직 
                showRefreshVoteList()
            }

            is VoteMainContract.VoteMainEvent.onAddAgendaInBoxClicked -> {
                sendEffect({ VoteMainContract.VoteMainSideEffect.NaviAgendaAdd })
            }

            is VoteMainContract.VoteMainEvent.OnClickDropBoxItem -> {
                //..//
                //상태 업데이트
                updateState {
                    copy(
                        groupId = event.member.shareGroupId,
                        groupName = event.member.name,
                        voteList = listOf()
                    )
                }
                setEvent(VoteMainContract.VoteMainEvent.OnPagingVoteList)
            }
    //..//

코드 더보기 :
나o만 GitHub Repository

소감

MVI 패턴을 적용해서 가독성이 좋아서 코드를 리뷰하기 매우매우매우매우 편하다. 협업하기에 좋은 패턴이라고 생각한다. 😎✌️

0개의 댓글