Skip to content

아키텍처

MVI/MVVM Multi-module Clean Architecture

truloop Android는 MVI(Model-View-Intent) 패턴과 MVVM 패턴을 결합한 아키텍처를 사용합니다. StateFlow 기반의 단방향 데이터 흐름으로 UI 상태를 관리합니다.

아키텍처 다이어그램

UiState / UiEvent / SideEffect 패턴

모든 Feature 모듈의 ViewModel은 3가지 구성 요소로 단방향 데이터 흐름을 관리합니다.

UiState

화면의 현재 상태를 표현하는 Immutable data class입니다.

kotlin
@Immutable
internal data class HomeUiState(
    val me: CurrentUser = CurrentUser.EMPTY,
    val selectedTabIndex: Int = 0,
    val error: TruloopError? = null,
    val hasNotificationPermission: Boolean = false,
    val showNotificationPermissionDialog: Boolean = false,
    val notificationPermissionRequested: Boolean = false,
)
  • @Immutable 어노테이션으로 Compose recomposition 최적화
  • MutableStateFlow로 관리하며 asStateFlow()로 읽기 전용 노출
  • update {} 함수를 통한 안전한 상태 변경

UiEvent

사용자 액션을 표현하는 sealed interface입니다.

kotlin
internal sealed interface HomeUiEvent {
    data object OnErrorDismiss : HomeUiEvent
    data class OnRouteChanged(val route: String) : HomeUiEvent
    data class ClickBottomBar(val bottomNavItem: BottomNavItem) : HomeUiEvent
    data class OnNotificationPermissionResult(val isGranted: Boolean) : HomeUiEvent
    data object DismissNotificationPermissionDialog : HomeUiEvent
    data object OnNotificationAlertConfirm : HomeUiEvent
    data object CheckInvite : HomeUiEvent
    data class InitBottomTab(val tab: String?) : HomeUiEvent
    data class RecordException(val exception: Throwable) : HomeUiEvent
}
  • View에서 발생하는 모든 이벤트를 하나의 onUiEvent() 함수로 처리
  • when expression으로 exhaustive matching 보장

SideEffect

일회성 이벤트(네비게이션, Toast 등)를 표현하는 sealed interface입니다.

kotlin
internal sealed interface HomeSideEffect {
    data class NavigateToNavKey(val key: Any) : HomeSideEffect
    data object OpenAppSettings : HomeSideEffect
}
  • Channel을 통해 전달되며 receiveAsFlow()로 수집
  • 화면 회전 등 Configuration Change에서 이벤트 유실 방지

데이터 흐름

ViewModel 패턴

kotlin
@HiltViewModel
@Stable
internal class HomeViewModel @Inject constructor(
    private val env: HomeEnv,
    private val homeSharedRepository: HomeSharedRepository,
    private val deepLinkRepository: DeepLinkRepository,
    private val deepLinkParser: DeepLinkParser,
    private val homeEventTracker: HomeEventTracker
) : ViewModel() {

    // 1. UI 상태
    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState = _uiState.asStateFlow()

    // 2. 일회성 이벤트
    private val _sideEffect = Channel<HomeSideEffect>()
    val sideEffect = _sideEffect.receiveAsFlow()

    // 3. 에러 핸들러
    private val exceptionHandler = truloopExceptionHandler { error ->
        _uiState.update { it.copy(error = error) }
    }

    // 4. 이벤트 처리
    fun onUiEvent(uiEvent: HomeUiEvent) {
        when (uiEvent) {
            HomeUiEvent.OnErrorDismiss -> {
                _uiState.update { it.copy(error = null) }
            }
            is HomeUiEvent.ClickBottomBar -> { /* ... */ }
            // ...
        }
    }

    // 5. 비동기 작업
    private fun fetchUserMe() {
        viewModelScope.launch(exceptionHandler) {
            env.refreshUser()
        }
    }
}

주요 특징

특징설명
@HiltViewModelHilt를 통한 ViewModel 자동 주입
@StableCompose 안정성 마킹으로 불필요한 recomposition 방지
truloopExceptionHandler공통 에러 처리 CoroutineExceptionHandler
viewModelScopeViewModel 생명주기에 바인딩된 CoroutineScope
Env 패턴Feature별 UseCase 의존성을 data class + Hilt Module로 캡슐화

Env 패턴

각 Feature의 외부 의존성(UseCase)을 Env data class로 캡슐화하고, Hilt @Module을 통해 제공합니다. ViewModel의 생성자를 간결하게 유지하면서도 UseCase 단위의 의존성을 명확히 합니다:

kotlin
// Env는 UseCase fun interface를 모아두는 data class
data class HomeEnv(
    val refreshUser: RefreshCurrentUserUseCase,
    val observeUser: ObserveUserUseCase,
    val joinLoop: JoinLoopUseCase
)

// Hilt Module에서 @Provides로 Env 인스턴스를 생성
@Module
@InstallIn(ViewModelComponent::class)
class HomeEnvModule {

    @Provides
    fun providesEnv(
        @RefreshCurrentUserIdentifier refreshCurrentUserUseCase: RefreshCurrentUserUseCase,
        @ObserveUserIdentifier observeCurrentUser: ObserveUserUseCase,
        @JoinLoopIdentifier joinLoop: JoinLoopUseCase,
    ): HomeEnv {
        return HomeEnv(
            refreshUser = refreshCurrentUserUseCase,
            observeUser = observeCurrentUser,
            joinLoop = joinLoop
        )
    }
}

Domain 모듈의 UseCase는 fun interface로 정의되며, @Qualifier 어노테이션으로 식별됩니다:

kotlin
// domain:user/UseCases.kt
fun interface RefreshCurrentUserUseCase {
    suspend operator fun invoke()
}

@Qualifier
annotation class RefreshCurrentUserIdentifier

Jetpack Navigation 3 (1.0.1)을 사용하며, 각 Feature 모듈은 자체 Navigation 그래프를 정의합니다:

kotlin
// feature/home/HomeNavigation.kt
fun homeNavigation(): NavEntry { /* ... */ }

// app/navigation/AppNavGraph.kt
// 모든 Feature의 Navigation을 조합

NavKey 기반으로 타입 안전한 네비게이션을 제공하며, core:navigation 모듈에서 공통 NavKey를 정의합니다.

변경 이력

날짜내용
2026-03-10Env 패턴 설명을 실제 코드 기준으로 전면 수정 (@Inject constructordata class + Hilt @Module/@Provides + UseCase fun interface). HomeUiEvent 예제에 누락된 이벤트 추가. truloopErrorTruloopError 오타 교정