Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
32cadc2
(CHORE)[#554] 'core:ui' 'core.router.routerApi' 의존성 추가
squart300kg Aug 22, 2025
6cf9af9
(FEAT)[#554] BaseViewModel.kt 정의
squart300kg Aug 22, 2025
abb6816
(REFACTOR)[#554] MainViewModel.kt에 BaseViewModel.kt 상속 처리 후 코드 삭제. 그에…
squart300kg Aug 22, 2025
0b679d5
(FIX)[#554] BaseViewModel내, Navigator 테스트 가능하도록 수정
squart300kg Aug 22, 2025
89ff34c
(TEST)[#554] MainViewModelTest 수정
squart300kg Aug 22, 2025
457345f
(REFACTOR)[#554] BookmarkViewModel.kt에 BaseViewModel.kt 상속 처리 후 코드 삭제.
squart300kg Aug 22, 2025
4c04ebe
(TEST)[#554] BookmarkViewModelTest.kt 수정
squart300kg Aug 22, 2025
0eb4165
(REFACTOR)[#554] SessionDetailViewModel.kt에 BaseViewModel.kt 상속 처리 후 …
squart300kg Aug 22, 2025
0b6b2a8
(TEST)[#554] SessionDetailViewModelTest.kt 수정
squart300kg Aug 22, 2025
deeb8b8
(REFACTOR)[#554] SessionListViewModel.kt에 BaseViewModel.kt 상속 처리 후 코드…
squart300kg Aug 22, 2025
89b944d
(TEST)[#554] SessionListViewModelTest.kt 수정
squart300kg Aug 22, 2025
75db57b
(REFACTOR)[#554] HomeViewModel.kt에 BaseViewModel.kt 상속 처리 후 코드 삭제. 그에…
squart300kg Aug 22, 2025
1c66255
(TEST)[#554] HomeViewModelTest.kt 수정
squart300kg Aug 22, 2025
08cb42b
(REFACTOR)[#554] ContributorViewModel.kt에 BaseViewModel.kt 상속 처리 후 코드…
squart300kg Aug 22, 2025
980f8fa
(TEST)[#554] ContributorViewModelTest.kt 수정
squart300kg Aug 22, 2025
4f0277b
(STYLE)[#554] detekt 적용
squart300kg Aug 22, 2025
533abc7
(STYLE)[#554] detekt 적용을 위한 백킹 프로퍼티 네이밍 변경(_errorFlow -> internalFlow)
squart300kg Aug 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ android {
dependencies {
implementation(projects.core.designsystem)
implementation(projects.core.domain.domainSessionApi)
implementation(projects.core.router.routerApi)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.droidknights.app.core.ui

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app.core.router.api.Navigator
import com.droidknights.app.core.router.api.model.Route
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
open class BaseViewModel @Inject constructor() : ViewModel() {
protected val internalErrorFlow = MutableSharedFlow<Throwable>()
val errorFlow = internalErrorFlow.asSharedFlow()

@Inject
lateinit var navigator: Navigator

@VisibleForTesting
fun injectNavigator(navigator: Navigator) {
this.navigator = navigator
}

fun navigateBack() = viewModelScope.launch {
navigator.navigateBack()
}

fun navigateWeb(url: String) = viewModelScope.launch {
navigator.navigateWeb(url)
}

fun navigateTo(
route: Route,
saveState: Boolean = false,
launchSingleTop: Boolean = false
) = viewModelScope.launch {
navigator.navigate(route, saveState, launchSingleTop)
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package com.droidknights.app.feature.bookmark

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app.core.domain.session.usecase.api.DeleteBookmarkedSessionUseCase
import com.droidknights.app.core.domain.session.usecase.api.GetBookmarkedSessionsUseCase
import com.droidknights.app.core.model.session.Session
import com.droidknights.app.core.router.api.Navigator
import com.droidknights.app.core.ui.BaseViewModel
import com.droidknights.app.feature.bookmark.model.BookmarkItemUiState
import com.droidknights.app.feature.bookmark.model.BookmarkUiState
import com.droidknights.app.feature.session.api.RouteSession
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
Expand All @@ -29,12 +26,8 @@ import javax.inject.Inject
@HiltViewModel
class BookmarkViewModel @Inject constructor(
private val getBookmarkedSessionsUseCase: GetBookmarkedSessionsUseCase,
private val deleteBookmarkedSessionUseCase: DeleteBookmarkedSessionUseCase,
private val navigator: Navigator,
) : ViewModel() {

private val _errorFlow = MutableSharedFlow<Throwable>()
val errorFlow = _errorFlow.asSharedFlow()
private val deleteBookmarkedSessionUseCase: DeleteBookmarkedSessionUseCase
) : BaseViewModel() {

private val _bookmarkUiState = MutableStateFlow<BookmarkUiState>(BookmarkUiState.Loading)
val bookmarkUiState = _bookmarkUiState.asStateFlow()
Expand Down Expand Up @@ -74,7 +67,7 @@ class BookmarkViewModel @Inject constructor(
}
}
}.catch { throwable ->
_errorFlow.emit(throwable)
internalErrorFlow.emit(throwable)
}.collect { _bookmarkUiState.value = it }
}
}
Expand Down Expand Up @@ -123,7 +116,7 @@ class BookmarkViewModel @Inject constructor(
state.copy(selectedSessionIds = persistentSetOf())
}
}.catch { throwable ->
_errorFlow.emit(throwable)
internalErrorFlow.emit(throwable)
}.launchIn(viewModelScope)
}

Expand All @@ -132,10 +125,8 @@ class BookmarkViewModel @Inject constructor(
if (state !is BookmarkUiState.Success || state.isEditMode) {
return
}
viewModelScope.launch {
navigator.navigate(
route = RouteSession(sessionId = session.id),
)
}
navigateTo(
route = RouteSession(sessionId = session.id)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ class BookmarkViewModelTest {
// given & when
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

// then
viewModel.bookmarkUiState.test {
Expand All @@ -76,9 +75,8 @@ class BookmarkViewModelTest {
// given
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

viewModel.bookmarkUiState.test {
val initialIsEditMode = (awaitItem() as BookmarkUiState.Success).isEditMode
Expand All @@ -98,9 +96,8 @@ class BookmarkViewModelTest {
// given
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

// when
viewModel.selectSession(mockSession1)
Expand All @@ -120,9 +117,9 @@ class BookmarkViewModelTest {
// given
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

viewModel.selectSession(mockSession1)

// when
Expand All @@ -144,9 +141,9 @@ class BookmarkViewModelTest {
coEvery { deleteBookmarkedSessionUseCase(persistentSetOf("1")) } just Runs
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

viewModel.selectSession(mockSession1)

// when
Expand All @@ -167,9 +164,8 @@ class BookmarkViewModelTest {
coEvery { navigator.navigate(RouteSession(sessionId = mockSession2.id)) } just Runs
viewModel = BookmarkViewModel(
getBookmarkedSessionsUseCase,
deleteBookmarkedSessionUseCase,
navigator
)
deleteBookmarkedSessionUseCase
).also { it.injectNavigator(navigator) }

// when
viewModel.redirectToSessionScreen(mockSession2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,35 @@
package com.droidknights.app.feature.contributor

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app.core.domain.contributor.usecase.api.GetContributorsUseCase
import com.droidknights.app.core.router.api.Navigator
import com.droidknights.app.core.ui.BaseViewModel
import com.droidknights.app.feature.contributor.model.ContributorsUiState
import com.droidknights.app.feature.contributor.model.convert.toContributorsUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ContributorViewModel @Inject constructor(
getContributorsUseCase: GetContributorsUseCase,
private val navigator: Navigator,
) : ViewModel() {

private val _errorFlow = MutableSharedFlow<Throwable>()
val errorFlow = _errorFlow.asSharedFlow()
getContributorsUseCase: GetContributorsUseCase
) : BaseViewModel() {

val uiState: StateFlow<ContributorsUiState> by lazy {
getContributorsUseCase()
.map {
it.toContributorsUiState()
}
.catch { throwable ->
_errorFlow.emit(throwable)
internalErrorFlow.emit(throwable)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ContributorsUiState.Loading
)
}

fun navigateBack() = viewModelScope.launch {
navigator.navigateBack()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ internal class ContributorViewModelTest {
fun `컨트리뷰터 데이터를 확인할 수 있다`() = runTest {
// given
coEvery { getContributorsUseCase() } returns flowOf(fakeContributors)
viewModel = ContributorViewModel(getContributorsUseCase, navigator)
viewModel = ContributorViewModel(getContributorsUseCase).also {
it.injectNavigator(navigator)
}

// when & then
viewModel.uiState.test {
Expand All @@ -44,7 +46,9 @@ internal class ContributorViewModelTest {
fun `navigateBack이 호출될 때 navigator에게 위임한다`() = runTest {
// suspend 함수 호출에 대한 stub
coEvery { navigator.navigateBack() } just Runs
viewModel = ContributorViewModel(getContributorsUseCase, navigator)
viewModel = ContributorViewModel(getContributorsUseCase).also {
it.injectNavigator(navigator)
}

// when
viewModel.navigateBack()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.droidknights.app.feature.contributor.api.RouteContributor
import com.droidknights.app.feature.home.component.ContributorCard
import com.droidknights.app.feature.home.component.SessionCard
import com.droidknights.app.feature.home.component.SponsorCard
import com.droidknights.app.feature.home.model.SponsorsUiState
import com.droidknights.app.feature.session.api.RouteSession
import kotlinx.coroutines.flow.collectLatest

@Composable
Expand All @@ -35,9 +37,9 @@ internal fun HomeRoute(
HomeScreen(
padding = padding,
sponsorsUiState = sponsorsUiState,
onSessionClick = viewModel::navigateSession,
onContributorClick = viewModel::navigateContributor,
onOrganizationSponsorClick = viewModel::navigateOrganizationSponsor,
onSessionClick = { viewModel.navigateTo(RouteSession()) },
onContributorClick = { viewModel.navigateTo(RouteContributor) },
onOrganizationSponsorClick = viewModel::navigateWeb,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
package com.droidknights.app.feature.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app.core.domain.sponsor.usecase.api.GetSponsorsUseCase
import com.droidknights.app.core.router.api.Navigator
import com.droidknights.app.feature.contributor.api.RouteContributor
import com.droidknights.app.core.ui.BaseViewModel
import com.droidknights.app.feature.home.model.SponsorsUiState
import com.droidknights.app.feature.session.api.RouteSession
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor(
getSponsorsUseCase: GetSponsorsUseCase,
private val navigator: Navigator,
) : ViewModel() {

private val _errorFlow = MutableSharedFlow<Throwable>()
val errorFlow get() = _errorFlow.asSharedFlow()
getSponsorsUseCase: GetSponsorsUseCase
) : BaseViewModel() {

val sponsorsUiState: StateFlow<SponsorsUiState> =
flow { emit(getSponsorsUseCase()) }
Expand All @@ -39,23 +29,11 @@ class HomeViewModel @Inject constructor(
}
}
.catch { throwable ->
_errorFlow.emit(throwable)
internalErrorFlow.emit(throwable)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SponsorsUiState.Loading,
)

fun navigateSession() = viewModelScope.launch {
navigator.navigate(RouteSession())
}

fun navigateContributor() = viewModelScope.launch {
navigator.navigate(RouteContributor)
}

fun navigateOrganizationSponsor(url: String) = viewModelScope.launch {
navigator.navigateWeb(url)
}
}
Loading
Loading