다크 모드
아키텍처
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()함수로 처리 whenexpression으로 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()
}
}
}주요 특징
| 특징 | 설명 |
|---|---|
@HiltViewModel | Hilt를 통한 ViewModel 자동 주입 |
@Stable | Compose 안정성 마킹으로 불필요한 recomposition 방지 |
truloopExceptionHandler | 공통 에러 처리 CoroutineExceptionHandler |
viewModelScope | ViewModel 생명주기에 바인딩된 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 RefreshCurrentUserIdentifierNavigation
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-10 | Env 패턴 설명을 실제 코드 기준으로 전면 수정 (@Inject constructor → data class + Hilt @Module/@Provides + UseCase fun interface). HomeUiEvent 예제에 누락된 이벤트 추가. truloopError → TruloopError 오타 교정 |