diff --git a/app/src/commonMain/kotlin/id/gdg/app/App.kt b/app/src/commonMain/kotlin/id/gdg/app/App.kt index 38753eb..d098885 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/App.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/App.kt @@ -11,20 +11,25 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import id.gdg.app.di.ViewModelFactory -import id.gdg.app.ui.AppEvent -import id.gdg.app.ui.EventDetailRouter -import id.gdg.app.ui.HomeRouter -import id.gdg.app.ui.OnboardingRouter +import id.gdg.app.ui.main.MainEvent import id.gdg.app.ui.ScreenScaffold -import id.gdg.app.ui.SplashScreenRouter -import id.gdg.app.ui.screen.EventDetailScreen -import id.gdg.app.ui.screen.MainScreen -import id.gdg.app.ui.screen.OnboardingScreen -import id.gdg.app.ui.screen.SplashScreen +import id.gdg.app.ui.detail.EventDetailRouter +import id.gdg.app.ui.detail.EventDetailScreen +import id.gdg.app.ui.detail.EventDetailViewModel +import id.gdg.app.ui.main.MainRouter +import id.gdg.app.ui.main.MainScreen +import id.gdg.app.ui.main.MainViewModel +import id.gdg.app.ui.onboarding.OnboardingRouter +import id.gdg.app.ui.onboarding.OnboardingScreen +import id.gdg.app.ui.onboarding.OnboardingViewModel +import id.gdg.app.ui.splash.SplashScreen +import id.gdg.app.ui.splash.SplashScreenRouter @Composable fun AppContent( - viewModel: AppViewModel = ViewModelFactory.create(), + onboardingViewModel: OnboardingViewModel = ViewModelFactory.onboardingViewModel(), + mainViewModel: MainViewModel = ViewModelFactory.mainViewModel(), + eventDetailViewModel: EventDetailViewModel = ViewModelFactory.eventDetailViewModel(), navController: NavHostController = rememberNavController() ) { Scaffold { innerPadding -> @@ -47,12 +52,9 @@ fun AppContent( composable { OnboardingScreen( - chapterList = viewModel.chapterList, - onChapterSelected = { chapterId -> - viewModel.sendEvent(AppEvent.ChangeChapterId(chapterId)) - }, + viewModel = onboardingViewModel, navigateToMainScreen = { - navController.navigate(HomeRouter) { + navController.navigate(MainRouter) { popUpTo(OnboardingRouter) { inclusive = true } @@ -61,16 +63,15 @@ fun AppContent( ) } - composable { + composable { ScreenScaffold( - viewModel = viewModel, - mainScreen = { viewModel, onEventDetailClicked -> + mainScreen = { onEventDetailClicked -> MainScreen( - viewModel = viewModel, + viewModel = mainViewModel, onEventDetailClicked = onEventDetailClicked ) }, - detailScreen = { viewModel, eventId -> + detailScreen = { eventId -> /** * Need to show the detail screen on same composable screen * due to side-to-side scaffold for tablet nor large screens. @@ -80,8 +81,11 @@ fun AppContent( * due to side-to-side scaffold for tablet nor large screens. */ EventDetailScreen( - viewModel = viewModel, - eventId = eventId + viewModel = eventDetailViewModel, + eventId = eventId, + onBack = { + + } ) }, navigateToDetailScreen = { eventId -> @@ -94,8 +98,11 @@ fun AppContent( val eventDetail = it.toRoute() EventDetailScreen( - viewModel = viewModel, - eventId = eventDetail.eventId + viewModel = eventDetailViewModel, + eventId = eventDetail.eventId, + onBack = { + + } ) } } diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/common/UiState.kt b/app/src/commonMain/kotlin/id/gdg/app/common/UiState.kt similarity index 92% rename from app/src/commonMain/kotlin/id/gdg/app/ui/state/common/UiState.kt rename to app/src/commonMain/kotlin/id/gdg/app/common/UiState.kt index 66d18f6..6e56171 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/common/UiState.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/common/UiState.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.state.common +package id.gdg.app.common sealed class UiState { data object Success : UiState() diff --git a/app/src/commonMain/kotlin/id/gdg/app/data/BottomNavBar.kt b/app/src/commonMain/kotlin/id/gdg/app/data/BottomNavBar.kt index cb6e925..f9e00b3 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/data/BottomNavBar.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/data/BottomNavBar.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import id.gdg.app.androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope import kotlin.jvm.JvmInline -val HomepageMenu = SelectedId(0) +val MainMenu = SelectedId(0) val ChapterInfoMenu = SelectedId(1) val ProfileMenu = SelectedId(2) @@ -32,7 +32,7 @@ object BottomNavBar { } private fun create() = listOf( - NavItem(HomepageMenu, "Home", Icons.Filled.Home), + NavItem(MainMenu, "Home", Icons.Filled.Home), NavItem(ChapterInfoMenu,"Chapter", Icons.Filled.Info), NavItem(ProfileMenu,"Profile", Icons.Filled.Person), ) diff --git a/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt b/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt index ad1b297..24585c6 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt @@ -2,7 +2,9 @@ package id.gdg.app.di import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel -import id.gdg.app.AppViewModel +import id.gdg.app.ui.main.MainViewModel +import id.gdg.app.ui.detail.EventDetailViewModel +import id.gdg.app.ui.onboarding.OnboardingViewModel import id.gdg.chapter.domain.GetChapterIdUseCase import id.gdg.chapter.domain.GetChapterListUseCase import id.gdg.chapter.domain.SetChapterIdUseCase @@ -25,14 +27,24 @@ object ViewModelFactory : KoinComponent { private val eventDetailUseCase: GetEventDetailUseCase by inject() @Composable - fun create() = viewModel { - AppViewModel( + fun onboardingViewModel() = viewModel { + OnboardingViewModel( chapterListUseCase, + setCurrentChapterUseCase + ) + } + + @Composable + fun mainViewModel() = viewModel { + MainViewModel( getCurrentChapterUseCase, setCurrentChapterUseCase, upComingEventUseCase, - previousEventUseCase, - eventDetailUseCase + previousEventUseCase ) } + @Composable + fun eventDetailViewModel() = viewModel { + EventDetailViewModel(eventDetailUseCase) + } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/AppEvent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/AppEvent.kt deleted file mode 100644 index 722d6bd..0000000 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/AppEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package id.gdg.app.ui - -sealed class AppEvent { - - data class ChangeChapterId(val chapterId: Int) : AppEvent() - - data object InitialContent : AppEvent() - data object FetchPreviousEvent : AppEvent() - data object FetchUpcomingEvent : AppEvent() - - data class EventDetail(val eventId: Int) : AppEvent() -} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/Router.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/Router.kt index 8642e21..919250e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/Router.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/Router.kt @@ -1,17 +1,3 @@ package id.gdg.app.ui -import kotlinx.serialization.Serializable - -sealed interface Router - -@Serializable -data object SplashScreenRouter : Router - -@Serializable -data object OnboardingRouter : Router - -@Serializable -data object HomeRouter : Router - -@Serializable -data class EventDetailRouter(val eventId: String) : Router \ No newline at end of file +interface Router \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt index 14907fd..34b50e9 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/ScreenScaffold.kt @@ -10,11 +10,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp -import id.gdg.app.AppViewModel +import id.gdg.app.ui.main.MainViewModel import id.gdg.app.androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import id.gdg.app.data.BottomNavBar import id.gdg.app.data.ChapterInfoMenu -import id.gdg.app.data.HomepageMenu +import id.gdg.app.data.MainMenu import id.gdg.app.data.ProfileMenu import id.gdg.ui.LocalWindowSizeClass import id.gdg.ui.TwoPanelScaffold @@ -22,13 +22,12 @@ import id.gdg.ui.TwoPanelScaffoldAnimationSpec @Composable fun ScreenScaffold( - viewModel: AppViewModel, - mainScreen: @Composable (AppViewModel, onEventDetailClicked: (String) -> Unit) -> Unit, - detailScreen: @Composable (AppViewModel, String) -> Unit, + mainScreen: @Composable (onEventDetailClicked: (String) -> Unit) -> Unit, + detailScreen: @Composable (String) -> Unit, navigateToDetailScreen: (String) -> Unit ) { var selectedEventId by rememberSaveable { mutableStateOf("") } - var selectedBottomNavItem by rememberSaveable { mutableStateOf(HomepageMenu) } + var selectedBottomNavItem by rememberSaveable { mutableStateOf(MainMenu) } val windowSizeClazz = LocalWindowSizeClass.current var shouldPanelOpened: Boolean? by rememberSaveable { mutableStateOf(null) } @@ -54,13 +53,13 @@ fun ScreenScaffold( ) { Surface { when (selectedBottomNavItem) { - HomepageMenu -> TwoPanelScaffold( + MainMenu -> TwoPanelScaffold( panelVisibility = panelVisibility, animationSpec = TwoPanelScaffoldAnimationSpec( finishedListener = { fraction -> if (fraction == 1f) shouldPanelOpened = null } ), body = { - mainScreen(viewModel) { + mainScreen { // If the screen size is compact (or mobile device screen size), then // navigate to detail page with router. Otherwise, render the [panel]. if (windowSizeClazz.widthSizeClass == WindowWidthSizeClass.Compact) { @@ -76,7 +75,7 @@ fun ScreenScaffold( panel = { Surface(tonalElevation = 1.dp) { if (shouldPanelOpened != null) { - detailScreen(viewModel, selectedEventId) + detailScreen(selectedEventId) } } } diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailRouter.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailRouter.kt new file mode 100644 index 0000000..bca0228 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailRouter.kt @@ -0,0 +1,7 @@ +package id.gdg.app.ui.detail + +import id.gdg.app.ui.Router +import kotlinx.serialization.Serializable + +@Serializable +data class EventDetailRouter(val eventId: String) : Router \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailScreen.kt similarity index 81% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailScreen.kt index 2e8177b..697d4da 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailScreen.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.screen +package id.gdg.app.ui.detail import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box @@ -10,16 +10,22 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import id.gdg.app.AppViewModel -import id.gdg.app.ui.AppEvent @Composable -fun EventDetailScreen(viewModel: AppViewModel, eventId: String) { +fun EventDetailScreen( + viewModel: EventDetailViewModel, + eventId: String, + onBack: () -> Unit +) { val eventDetailUiState by viewModel.eventDetailUiState.collectAsState() LaunchedEffect(eventId) { - if (eventId.isEmpty()) return@LaunchedEffect - viewModel.sendEvent(AppEvent.EventDetail(eventId.toInt())) + if (eventId.isEmpty()) { + onBack() + return@LaunchedEffect + } + + viewModel.fetch(eventId.toInt()) } Box { diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailUiModel.kt similarity index 80% rename from app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailUiModel.kt index 5afac40..3989cb7 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailUiModel.kt @@ -1,6 +1,6 @@ -package id.gdg.app.ui.state +package id.gdg.app.ui.detail -import id.gdg.app.ui.state.common.UiState +import id.gdg.app.common.UiState import id.gdg.event.model.EventDetailModel data class EventDetailUiModel( diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailViewModel.kt new file mode 100644 index 0000000..d775f5c --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/detail/EventDetailViewModel.kt @@ -0,0 +1,38 @@ +package id.gdg.app.ui.detail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import id.gdg.app.common.UiState +import id.gdg.app.common.asUiState +import id.gdg.event.domain.GetEventDetailUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class EventDetailViewModel( + private val eventDetailUseCase: GetEventDetailUseCase +) : ViewModel() { + + private var _eventDetailUiState = MutableStateFlow(EventDetailUiModel.Empty) + val eventDetailUiState get() = _eventDetailUiState.asStateFlow() + + fun fetch(eventId: Int) { + _eventDetailUiState.update { it.copy(state = UiState.Loading) } + + viewModelScope.launch { + val result = eventDetailUseCase(eventId) + + withContext(Dispatchers.Main) { + _eventDetailUiState.update { + it.copy( + state = result.asUiState(), + detail = result.getOrNull() + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainEvent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainEvent.kt new file mode 100644 index 0000000..f5f08a8 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainEvent.kt @@ -0,0 +1,11 @@ +package id.gdg.app.ui.main + +sealed class MainEvent { + + data class ChangeChapterId(val chapterId: Int) : MainEvent() + + data object InitialContent : MainEvent() + + data object FetchPreviousEvent : MainEvent() + data object FetchUpcomingEvent : MainEvent() +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainRouter.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainRouter.kt new file mode 100644 index 0000000..b828cb5 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainRouter.kt @@ -0,0 +1,7 @@ +package id.gdg.app.ui.main + +import id.gdg.app.ui.Router +import kotlinx.serialization.Serializable + +@Serializable +data object MainRouter : Router \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainScreen.kt similarity index 59% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/MainScreen.kt index cc0b3f9..efcabae 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainScreen.kt @@ -1,33 +1,26 @@ -package id.gdg.app.ui.screen +package id.gdg.app.ui.main import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import id.gdg.app.AppViewModel -import id.gdg.app.ui.AppEvent -import id.gdg.app.ui.screen.content.PreviousEventContent -import id.gdg.app.ui.screen.content.UpcomingEventContent +import id.gdg.app.ui.main.content.PreviousEventContent +import id.gdg.app.ui.main.content.UpcomingEventContent @Composable fun MainScreen( - viewModel: AppViewModel, + viewModel: MainViewModel, onEventDetailClicked: (String) -> Unit ) { val chapterUiState by viewModel.chapterUiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.sendEvent(AppEvent.InitialContent) - } - Column { UpcomingEventContent(chapterUiState.upcomingEvent) PreviousEventContent( data = chapterUiState.previousEvents, onRefreshContent = { - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + viewModel.sendEvent(MainEvent.FetchPreviousEvent) }, onEventClicked = { onEventDetailClicked(it) diff --git a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainViewModel.kt similarity index 62% rename from app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/MainViewModel.kt index 1d2ee7b..d965c8e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/MainViewModel.kt @@ -1,21 +1,18 @@ -package id.gdg.app +package id.gdg.app.ui.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import id.gdg.app.ui.AppEvent -import id.gdg.app.ui.state.ChapterUiModel -import id.gdg.app.ui.state.EventDetailUiModel -import id.gdg.app.ui.state.common.UiState -import id.gdg.app.ui.state.common.asUiState -import id.gdg.app.ui.state.partial.PreviousEventsUiModel -import id.gdg.app.ui.state.partial.UpcomingEventUiModel +import id.gdg.app.ui.main.state.ChapterUiModel +import id.gdg.app.ui.detail.EventDetailUiModel +import id.gdg.app.common.UiState +import id.gdg.app.common.asUiState +import id.gdg.app.ui.main.state.PreviousEventsUiModel +import id.gdg.app.ui.main.state.UpcomingEventUiModel import id.gdg.chapter.domain.GetChapterIdUseCase import id.gdg.chapter.domain.GetChapterListUseCase import id.gdg.chapter.domain.SetChapterIdUseCase -import id.gdg.event.domain.GetEventDetailUseCase import id.gdg.event.domain.GetPreviousEventUseCase import id.gdg.event.domain.GetUpcomingEventUseCase -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -25,29 +22,24 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class AppViewModel( +class MainViewModel( // Chapters - private val chapterListUseCase: GetChapterListUseCase, private val getCurrentChapterUseCase: GetChapterIdUseCase, private val setCurrentChapterUseCase: SetChapterIdUseCase, // Events private val upcomingEventUseCase: GetUpcomingEventUseCase, - private val previousEventUseCase: GetPreviousEventUseCase, - private val eventDetailUseCase: GetEventDetailUseCase, + private val previousEventUseCase: GetPreviousEventUseCase ) : ViewModel() { private var currentChapterId = 0 - private var _action = MutableSharedFlow(replay = 50) + private var _action = MutableSharedFlow(replay = 50) private var _upcomingEvent = MutableStateFlow(UpcomingEventUiModel.Empty) private var _previousEvents = MutableStateFlow(PreviousEventsUiModel.Empty) - val chapterList get() = chapterListUseCase() - val chapterUiState: StateFlow = combine( _upcomingEvent, _previousEvents @@ -70,7 +62,7 @@ class AppViewModel( } } - fun sendEvent(event: AppEvent) { + fun sendEvent(event: MainEvent) { _action.tryEmit(event) } @@ -80,21 +72,22 @@ class AppViewModel( .collect { val chapterId = it ?: return@collect currentChapterId = chapterId + + // immediately fetch init + sendEvent(MainEvent.InitialContent) } } } - private fun observeActionEvent(action: AppEvent) { + private fun observeActionEvent(action: MainEvent) { when (action) { - is AppEvent.ChangeChapterId -> shouldChangeCurrentChapterId(action.chapterId) - is AppEvent.InitialContent -> { - sendEvent(AppEvent.FetchPreviousEvent) - sendEvent(AppEvent.FetchUpcomingEvent) + is MainEvent.ChangeChapterId -> shouldChangeCurrentChapterId(action.chapterId) + is MainEvent.InitialContent -> { + sendEvent(MainEvent.FetchPreviousEvent) + sendEvent(MainEvent.FetchUpcomingEvent) } - - is AppEvent.FetchPreviousEvent -> fetchPreviousEvent(currentChapterId) - is AppEvent.FetchUpcomingEvent -> fetchUpcomingEvents(currentChapterId) - is AppEvent.EventDetail -> fetchEventDetail(action.eventId) + is MainEvent.FetchPreviousEvent -> fetchPreviousEvent(currentChapterId) + is MainEvent.FetchUpcomingEvent -> fetchUpcomingEvents(currentChapterId) } } @@ -133,21 +126,4 @@ class AppViewModel( } } } - - private fun fetchEventDetail(eventId: Int) { - _eventDetailUiState.update { it.copy(state = UiState.Loading) } - - viewModelScope.launch { - val result = eventDetailUseCase(eventId) - - withContext(Dispatchers.Main) { - _eventDetailUiState.update { - it.copy( - state = result.asUiState(), - detail = result.getOrNull() - ) - } - } - } - } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/content/PreviousEventContent.kt similarity index 95% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/content/PreviousEventContent.kt index 3cc0682..83dbaab 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/content/PreviousEventContent.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.screen.content +package id.gdg.app.ui.main.content import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box @@ -12,7 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import id.gdg.app.ui.state.partial.PreviousEventsUiModel +import id.gdg.app.ui.main.state.PreviousEventsUiModel import id.gdg.ui.component.EventSimpleCard import id.gdg.ui.component.HeadlineSection import id.gdg.ui.component.shimmer.EventSimpleCardShimmerList diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/UpcomingEventContent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/content/UpcomingEventContent.kt similarity index 59% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/UpcomingEventContent.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/content/UpcomingEventContent.kt index fd52a0b..1c150ae 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/UpcomingEventContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/content/UpcomingEventContent.kt @@ -1,7 +1,7 @@ -package id.gdg.app.ui.screen.content +package id.gdg.app.ui.main.content import androidx.compose.runtime.Composable -import id.gdg.app.ui.state.partial.UpcomingEventUiModel +import id.gdg.app.ui.main.state.UpcomingEventUiModel @Composable fun UpcomingEventContent( diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/ChapterUiModel.kt similarity index 69% rename from app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/state/ChapterUiModel.kt index 2eef368..7c568f4 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/ChapterUiModel.kt @@ -1,7 +1,4 @@ -package id.gdg.app.ui.state - -import id.gdg.app.ui.state.partial.PreviousEventsUiModel -import id.gdg.app.ui.state.partial.UpcomingEventUiModel +package id.gdg.app.ui.main.state data class ChapterUiModel( val upcomingEvent: UpcomingEventUiModel, diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/PreviousEventsUiModel.kt similarity index 74% rename from app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/state/PreviousEventsUiModel.kt index 41b80de..855bdac 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/PreviousEventsUiModel.kt @@ -1,7 +1,7 @@ -package id.gdg.app.ui.state.partial +package id.gdg.app.ui.main.state -import id.gdg.app.ui.screen.uimodel.toEventContent -import id.gdg.app.ui.state.common.UiState +import id.gdg.app.ui.main.uimodel.toEventContent +import id.gdg.app.common.UiState import id.gdg.event.model.EventModel data class PreviousEventsUiModel( diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/UpcomingEventUiModel.kt similarity index 79% rename from app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/state/UpcomingEventUiModel.kt index a80dbe8..b8c1940 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/state/UpcomingEventUiModel.kt @@ -1,6 +1,6 @@ -package id.gdg.app.ui.state.partial +package id.gdg.app.ui.main.state -import id.gdg.app.ui.state.common.UiState +import id.gdg.app.common.UiState import id.gdg.event.model.EventModel data class UpcomingEventUiModel( diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/main/uimodel/EventContent.mapper.kt similarity index 89% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/main/uimodel/EventContent.mapper.kt index 21def42..b6ed85c 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/main/uimodel/EventContent.mapper.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.screen.uimodel +package id.gdg.app.ui.main.uimodel import id.gdg.event.model.EventModel import id.gdg.ui.component.EventContent diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingRouter.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingRouter.kt new file mode 100644 index 0000000..e57479a --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingRouter.kt @@ -0,0 +1,7 @@ +package id.gdg.app.ui.onboarding + +import id.gdg.app.ui.Router +import kotlinx.serialization.Serializable + +@Serializable +data object OnboardingRouter : Router \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/OnboardingScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingScreen.kt similarity index 96% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/OnboardingScreen.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingScreen.kt index 359c658..ed1174e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/OnboardingScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingScreen.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.screen +package id.gdg.app.ui.onboarding import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image @@ -40,8 +40,7 @@ import org.jetbrains.compose.resources.stringResource @Composable fun OnboardingScreen( - chapterList: List, - onChapterSelected: (Int) -> Unit, + viewModel: OnboardingViewModel, navigateToMainScreen: () -> Unit, modifier: Modifier = Modifier ) { @@ -55,8 +54,10 @@ fun OnboardingScreen( footerOnboardingContent( ref = footer, - chapterList = chapterList, - onChapterSelected = onChapterSelected, + chapterList = viewModel.chapterList, + onChapterSelected = { + viewModel.setChapterId(it) + }, navigateToMainScreen = navigateToMainScreen ) } diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingViewModel.kt new file mode 100644 index 0000000..ed27114 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/onboarding/OnboardingViewModel.kt @@ -0,0 +1,21 @@ +package id.gdg.app.ui.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import id.gdg.chapter.domain.GetChapterListUseCase +import id.gdg.chapter.domain.SetChapterIdUseCase +import kotlinx.coroutines.launch + +class OnboardingViewModel( + private val chapterListUseCase: GetChapterListUseCase, + private val setCurrentChapterUseCase: SetChapterIdUseCase +) : ViewModel() { + + val chapterList get() = chapterListUseCase() + + fun setChapterId(id: Int) { + viewModelScope.launch { + setCurrentChapterUseCase(id) + } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/SplashScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreen.kt similarity index 97% rename from app/src/commonMain/kotlin/id/gdg/app/ui/screen/SplashScreen.kt rename to app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreen.kt index 62fb07e..e5e863e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/SplashScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreen.kt @@ -1,4 +1,4 @@ -package id.gdg.app.ui.screen +package id.gdg.app.ui.splash import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreenRouter.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreenRouter.kt new file mode 100644 index 0000000..d040817 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/splash/SplashScreenRouter.kt @@ -0,0 +1,7 @@ +package id.gdg.app.ui.splash + +import id.gdg.app.ui.Router +import kotlinx.serialization.Serializable + +@Serializable +data object SplashScreenRouter : Router \ No newline at end of file diff --git a/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt b/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt index 39b1713..b35718f 100644 --- a/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt +++ b/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt @@ -2,11 +2,11 @@ package id.gdg.app import app.cash.turbine.test import id.gdg.app.robot.AppViewModelRobot -import id.gdg.app.ui.AppEvent -import id.gdg.app.ui.state.ChapterUiModel -import id.gdg.app.ui.state.common.UiState -import id.gdg.app.ui.state.partial.PreviousEventsUiModel -import id.gdg.app.ui.state.partial.UpcomingEventUiModel +import id.gdg.app.ui.main.MainEvent +import id.gdg.app.ui.main.state.ChapterUiModel +import id.gdg.app.common.UiState +import id.gdg.app.ui.main.state.PreviousEventsUiModel +import id.gdg.app.ui.main.state.UpcomingEventUiModel import id.gdg.event.model.EventDetailModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,7 +42,7 @@ class AppViewModelTest : KoinTest { @Test fun `when ChangeChapterId is invoked then selected chapter ID is returned`() { val expectedValue = 1 - viewModel.sendEvent(AppEvent.ChangeChapterId(expectedValue)) + viewModel.sendEvent(MainEvent.ChangeChapterId(expectedValue)) runBlocking { robot.getCurrentChapterUseCase().test { @@ -69,8 +69,8 @@ class AppViewModelTest : KoinTest { ) ) - viewModel.sendEvent(AppEvent.ChangeChapterId(1)) - viewModel.sendEvent(AppEvent.InitialContent) + viewModel.sendEvent(MainEvent.ChangeChapterId(1)) + viewModel.sendEvent(MainEvent.InitialContent) runBlocking { viewModel.chapterUiState.test { @@ -84,7 +84,7 @@ class AppViewModelTest : KoinTest { val events = robot.createEvents() robot.previousEventUseCase.setData(Result.success(events)) - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + viewModel.sendEvent(MainEvent.FetchPreviousEvent) runBlocking { viewModel.chapterUiState.test { @@ -99,7 +99,7 @@ class AppViewModelTest : KoinTest { fun `when FetchPreviousEvent is invoked and no previous events exist then an empty list is returned`() { robot.previousEventUseCase.setData(Result.success(emptyList())) - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + viewModel.sendEvent(MainEvent.FetchPreviousEvent) runBlocking { viewModel.chapterUiState.test { @@ -114,7 +114,7 @@ class AppViewModelTest : KoinTest { fun `when FetchPreviousEvent is invoked and a network error occurs then a Fail state is returned`() { robot.previousEventUseCase.setData(Result.failure(Throwable("network error"))) - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + viewModel.sendEvent(MainEvent.FetchPreviousEvent) runBlocking { viewModel.chapterUiState.test { @@ -128,7 +128,7 @@ class AppViewModelTest : KoinTest { val events = robot.createEvents() robot.upcomingEventUseCase.setData(Result.success(events.first())) - viewModel.sendEvent(AppEvent.FetchUpcomingEvent) + viewModel.sendEvent(MainEvent.FetchUpcomingEvent) runBlocking { viewModel.chapterUiState.test { @@ -143,7 +143,7 @@ class AppViewModelTest : KoinTest { fun `when FetchUpcomingEvent is invoked and no upcoming event exists then an empty event is returned`() { robot.upcomingEventUseCase.setData(Result.success(null)) - viewModel.sendEvent(AppEvent.FetchUpcomingEvent) + viewModel.sendEvent(MainEvent.FetchUpcomingEvent) runBlocking { viewModel.chapterUiState.test { @@ -158,7 +158,7 @@ class AppViewModelTest : KoinTest { fun `when FetchUpcomingEvent is invoked and a network error occurs then a Fail state is returned`() { robot.upcomingEventUseCase.setData(Result.failure(Throwable("network error"))) - viewModel.sendEvent(AppEvent.FetchUpcomingEvent) + viewModel.sendEvent(MainEvent.FetchUpcomingEvent) runBlocking { viewModel.chapterUiState.test { @@ -175,7 +175,7 @@ class AppViewModelTest : KoinTest { robot.eventDetailUseCase.setData(Result.success(expectedValue)) - viewModel.sendEvent(AppEvent.EventDetail(1)) + viewModel.sendEvent(MainEvent.EventDetail(1)) runBlocking { viewModel.eventDetailUiState.test { @@ -190,7 +190,7 @@ class AppViewModelTest : KoinTest { fun `when EventDetail is invoked and invalid event id then an empty event detail is returned`() { robot.eventDetailUseCase.setData(Result.success(null)) - viewModel.sendEvent(AppEvent.EventDetail(-1)) + viewModel.sendEvent(MainEvent.EventDetail(-1)) runBlocking { viewModel.eventDetailUiState.test { @@ -205,7 +205,7 @@ class AppViewModelTest : KoinTest { fun `when EventDetail is invoked and a network error occurs then a Fail state is returned`() { robot.eventDetailUseCase.setData(Result.failure(Throwable("network error"))) - viewModel.sendEvent(AppEvent.EventDetail(0)) + viewModel.sendEvent(MainEvent.EventDetail(0)) runBlocking { viewModel.eventDetailUiState.test { diff --git a/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt b/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt index 5f91b55..86822d8 100644 --- a/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt +++ b/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt @@ -3,7 +3,7 @@ package id.gdg.app.robot import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences -import id.gdg.app.AppViewModel +import id.gdg.app.ui.main.MainViewModel import id.gdg.app.di.appModule import id.gdg.app.stub.ChapterSelectionLocalStoreStub import id.gdg.app.stub.GetChapterListUseCaseStub @@ -46,7 +46,7 @@ object AppViewModelRobot : KoinTest { val getCurrentChapterUseCase by lazy { GetChapterIdUseCaseImpl(localStore) } private val setCurrentChapterUseCase by lazy { SetChapterIdUseCaseImpl(localStore) } - fun createViewModel() = AppViewModel( + fun createViewModel() = MainViewModel( chapterListUseCase, getCurrentChapterUseCase, setCurrentChapterUseCase,