From 79f13cfce9064de7b9996d7c6ba9d6e7d2837db6 Mon Sep 17 00:00:00 2001 From: isfaaghyth Date: Sat, 31 Aug 2024 14:48:33 +0700 Subject: [PATCH] feat: create update loop --- .../kotlin/id/gdg/app/AppContent.kt | 16 +- .../kotlin/id/gdg/app/AppViewModel.kt | 162 ++++-------------- .../gdg/app/{ui/state => }/common/UiState.kt | 2 +- .../kotlin/id/gdg/app/common/Update.kt | 47 +++++ .../id/gdg/app/common/UpdateableViewModel.kt | 23 +++ .../kotlin/id/gdg/app/di/ViewModelFactory.kt | 30 +--- .../kotlin/id/gdg/app/di/modules.kt | 17 ++ .../kotlin/id/gdg/app/ui/AppEvent.kt | 11 +- .../id/gdg/app/ui/screen/EventDetailScreen.kt | 22 +-- .../kotlin/id/gdg/app/ui/screen/MainScreen.kt | 39 +++-- .../ui/screen/content/PreviousEventContent.kt | 11 +- .../ui/screen/uimodel/EventContent.mapper.kt | 14 -- .../kotlin/id/gdg/app/ui/state/AppUiModel.kt | 6 + .../id/gdg/app/ui/state/ChapterUiModel.kt | 13 +- .../gdg/app/ui/state/EventChapterUiModel.kt | 17 ++ .../id/gdg/app/ui/state/EventDetailUiModel.kt | 2 +- .../ui/state/partial/PreviousEventsUiModel.kt | 9 +- .../ui/state/partial/UpcomingEventUiModel.kt | 6 +- .../id/gdg/app/update/Chapter.update.kt | 47 +++++ .../id/gdg/app/update/ChapterEvent.update.kt | 80 +++++++++ .../id/gdg/app/update/EventDetail.update.kt | 55 ++++++ .../kotlin/id/gdg/app/AppViewModelTest.kt | 83 +++------ .../id/gdg/app/robot/AppViewModelRobot.kt | 53 +++++- .../id/gdg/ui/component/EventSimpleCard.kt | 22 +-- 24 files changed, 496 insertions(+), 291 deletions(-) rename app/src/commonMain/kotlin/id/gdg/app/{ui/state => }/common/UiState.kt (92%) create mode 100644 app/src/commonMain/kotlin/id/gdg/app/common/Update.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/common/UpdateableViewModel.kt delete mode 100644 app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/ui/state/AppUiModel.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/ui/state/EventChapterUiModel.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/update/Chapter.update.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/update/ChapterEvent.update.kt create mode 100644 app/src/commonMain/kotlin/id/gdg/app/update/EventDetail.update.kt diff --git a/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt b/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt index ebf17cf..3c9c186 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/AppContent.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -24,6 +26,9 @@ fun AppContent( viewModel: AppViewModel = ViewModelFactory.create(), navController: NavHostController = rememberNavController() ) { + val uiState by viewModel.uiState.collectAsState() + val detailUiState by viewModel.eventDetailUiState.collectAsState() + Scaffold { innerPadding -> NavHost( navController = navController, @@ -43,7 +48,7 @@ fun AppContent( composable(route = AppRouter.OnboardingRoute) { OnboardingScreen( - chapterList = viewModel.chapterList, + chapterList = uiState.chapterUiState.chapters, onChapterSelected = { chapterId -> viewModel.sendEvent(AppEvent.ChangeChapterId(chapterId)) }, @@ -58,7 +63,11 @@ fun AppContent( composable(route = AppRouter.HomeRoute) { MainScreen( - viewModel = viewModel, + uiState = uiState, + detailUiState = detailUiState, + onSendEvent = { + viewModel.sendEvent(it) + }, navigateToDetailScreen = { eventId -> navController.navigate(AppRouter.constructEventDetailRoute(eventId)) } @@ -72,7 +81,8 @@ fun AppContent( val eventId = backStackEntry.arguments?.getString(AppRouter.ArgumentEventId).orEmpty() EventDetailScreen( - viewModel = viewModel, + model = detailUiState, + onSendEvent = { viewModel.sendEvent(it) }, eventId = eventId ) } diff --git a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt index 1d2ee7b..5673d01 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt @@ -1,153 +1,67 @@ package id.gdg.app -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import id.gdg.app.update.ChapterEventUpdate +import id.gdg.app.update.ChapterUpdate +import id.gdg.app.update.EventDetailUpdate +import id.gdg.app.common.Update +import id.gdg.app.common.UpdateableViewModel import id.gdg.app.ui.AppEvent -import id.gdg.app.ui.state.ChapterUiModel +import id.gdg.app.ui.state.AppUiModel 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.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 import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow 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( - // 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, -) : ViewModel() { - - private var currentChapterId = 0 - - 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 - ) { upcoming, previous -> - ChapterUiModel(upcoming, previous) + private val chapterUpdate: ChapterUpdate, + private val chapterEventUpdate: ChapterEventUpdate, + private val eventDetailUpdate: EventDetailUpdate +) : UpdateableViewModel() { + + val uiState: StateFlow = combine( + chapterUpdate.chapterUiState, + chapterEventUpdate.eventsUiState + ) { chapters, events -> + AppUiModel( + eventUiState = events, + chapterUiState = chapters + ) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - ChapterUiModel.Default + AppUiModel() ) - private var _eventDetailUiState = MutableStateFlow(EventDetailUiModel.Empty) - val eventDetailUiState get() = _eventDetailUiState.asStateFlow() - - init { - observeOnChapterIdChanged() - - viewModelScope.launch { - _action.collect(::observeActionEvent) - } - } - - fun sendEvent(event: AppEvent) { - _action.tryEmit(event) - } - - private fun observeOnChapterIdChanged() { - viewModelScope.launch { - getCurrentChapterUseCase() - .collect { - val chapterId = it ?: return@collect - currentChapterId = chapterId - } - } - } + val eventDetailUiState = eventDetailUpdate.eventDetailUiState + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + EventDetailUiModel.Empty + ) - private fun observeActionEvent(action: AppEvent) { - when (action) { - is AppEvent.ChangeChapterId -> shouldChangeCurrentChapterId(action.chapterId) - is AppEvent.InitialContent -> { - sendEvent(AppEvent.FetchPreviousEvent) - sendEvent(AppEvent.FetchUpcomingEvent) - } - - is AppEvent.FetchPreviousEvent -> fetchPreviousEvent(currentChapterId) - is AppEvent.FetchUpcomingEvent -> fetchUpcomingEvents(currentChapterId) - is AppEvent.EventDetail -> fetchEventDetail(action.eventId) - } - } - - private fun shouldChangeCurrentChapterId(chapterId: Int) { - viewModelScope.launch { - setCurrentChapterUseCase(chapterId) - } - } - - private fun fetchPreviousEvent(chapterId: Int) { - _previousEvents.update { it.copy(state = UiState.Loading) } - - viewModelScope.launch { - val result = previousEventUseCase(chapterId) + private var _action = MutableSharedFlow(replay = 50) - _previousEvents.update { - it.copy( - state = result.asUiState(), - previousEvents = result.getOrNull() ?: emptyList() - ) - } - } + override fun updates(): List { + return listOf( + chapterUpdate, + chapterEventUpdate, + eventDetailUpdate + ) } - private fun fetchUpcomingEvents(chapterId: Int) { - _upcomingEvent.update { it.copy(state = UiState.Loading) } + init { + setupLoop() viewModelScope.launch { - val result = upcomingEventUseCase(chapterId) - - _upcomingEvent.update { - it.copy( - state = result.asUiState(), - upcomingEvent = result.getOrNull() - ) - } + _action.collect(::eventHandlers) } } - 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() - ) - } - } - } + fun sendEvent(event: AppEvent) { + _action.tryEmit(event) } } \ No newline at end of file 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/common/Update.kt b/app/src/commonMain/kotlin/id/gdg/app/common/Update.kt new file mode 100644 index 0000000..048e8e4 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/common/Update.kt @@ -0,0 +1,47 @@ +package id.gdg.app.common + +import id.gdg.app.ui.AppEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +interface Update { + + fun handleEvent(event: AppEvent) +} + +@OptIn(ExperimentalStdlibApi::class) +interface UpdateScope : AutoCloseable { + + val scope: CoroutineScope + + fun shouldUseViewModelScope(scope: CoroutineScope) +} + +class MainUpdateScope : UpdateScope, CoroutineScope { + + private var viewModelScope: CoroutineScope? = null + + override val coroutineContext: CoroutineContext + get() = try { + Dispatchers.Main.immediate + } catch (_: NotImplementedError) { + EmptyCoroutineContext + } catch (_: IllegalStateException) { + EmptyCoroutineContext + } + SupervisorJob() + + override val scope: CoroutineScope + get() = viewModelScope ?: this + + override fun shouldUseViewModelScope(scope: CoroutineScope) { + viewModelScope = scope + } + + override fun close() { + coroutineContext.cancel() + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/common/UpdateableViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/common/UpdateableViewModel.kt new file mode 100644 index 0000000..06ff051 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/common/UpdateableViewModel.kt @@ -0,0 +1,23 @@ +package id.gdg.app.common + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import id.gdg.app.ui.AppEvent + +@OptIn(ExperimentalStdlibApi::class) +abstract class UpdateableViewModel() : ViewModel() { + + abstract fun updates(): List + + fun setupLoop() { + updates() + .map { it as UpdateScope } + .map { + it.shouldUseViewModelScope(viewModelScope) + addCloseable(it) + } + } + + fun eventHandlers(event: AppEvent) = + updates().forEach { it.handleEvent(event) } +} \ No newline at end of file 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..3c9e11d 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt @@ -3,36 +3,24 @@ package id.gdg.app.di import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import id.gdg.app.AppViewModel -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 id.gdg.app.update.ChapterEventUpdate +import id.gdg.app.update.ChapterUpdate +import id.gdg.app.update.EventDetailUpdate import org.koin.core.component.KoinComponent import org.koin.core.component.inject object ViewModelFactory : KoinComponent { - // Chapters - private val chapterListUseCase: GetChapterListUseCase by inject() - private val getCurrentChapterUseCase: GetChapterIdUseCase by inject() - private val setCurrentChapterUseCase: SetChapterIdUseCase by inject() - - // Events - private val upComingEventUseCase: GetUpcomingEventUseCase by inject() - private val previousEventUseCase: GetPreviousEventUseCase by inject() - private val eventDetailUseCase: GetEventDetailUseCase by inject() + private val chapterUpdate: ChapterUpdate by inject() + private val chapterEventUpdate: ChapterEventUpdate by inject() + private val eventDetailUpdate: EventDetailUpdate by inject() @Composable fun create() = viewModel { AppViewModel( - chapterListUseCase, - getCurrentChapterUseCase, - setCurrentChapterUseCase, - upComingEventUseCase, - previousEventUseCase, - eventDetailUseCase + chapterUpdate, + chapterEventUpdate, + eventDetailUpdate ) } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/di/modules.kt b/app/src/commonMain/kotlin/id/gdg/app/di/modules.kt index 02e18b6..eec6d35 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/di/modules.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/di/modules.kt @@ -1,9 +1,26 @@ package id.gdg.app.di +import id.gdg.app.common.MainUpdateScope +import id.gdg.app.common.UpdateScope +import id.gdg.app.update.ChapterEventUpdate +import id.gdg.app.update.ChapterEventUpdateImpl +import id.gdg.app.update.ChapterUpdate +import id.gdg.app.update.ChapterUpdateImpl +import id.gdg.app.update.EventDetailUpdate +import id.gdg.app.update.EventDetailUpdateImpl import id.gdg.chapter.di.chapterModule import id.gdg.event.di.eventModule +import org.koin.dsl.module + +val updateModule = module { + single { MainUpdateScope() } + single { ChapterUpdateImpl(get(), get(), get(), get()) } + single { EventDetailUpdateImpl(get(), get()) } + single { ChapterEventUpdateImpl(get(), get(), get()) } +} val appModule = listOf( + updateModule, chapterModule, eventModule, ) \ 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 index 722d6bd..11995d5 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/AppEvent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/AppEvent.kt @@ -1,12 +1,11 @@ package id.gdg.app.ui -sealed class AppEvent { +sealed interface AppEvent { - data class ChangeChapterId(val chapterId: Int) : AppEvent() + data class ChangeChapterId(val chapterId: Int) : AppEvent - data object InitialContent : AppEvent() - data object FetchPreviousEvent : AppEvent() - data object FetchUpcomingEvent : AppEvent() + data class FetchPreviousEvent(val chapterId: Int) : AppEvent + data class FetchUpcomingEvent(val chapterId: Int) : AppEvent - data class EventDetail(val eventId: Int) : 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/screen/EventDetailScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt index 2e8177b..120d9f5 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt @@ -8,30 +8,30 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text 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.state.EventDetailUiModel @Composable -fun EventDetailScreen(viewModel: AppViewModel, eventId: String) { - val eventDetailUiState by viewModel.eventDetailUiState.collectAsState() - +fun EventDetailScreen( + model: EventDetailUiModel, + onSendEvent: (AppEvent) -> Unit, + eventId: String +) { LaunchedEffect(eventId) { if (eventId.isEmpty()) return@LaunchedEffect - viewModel.sendEvent(AppEvent.EventDetail(eventId.toInt())) + onSendEvent(AppEvent.EventDetail(eventId.toInt())) } Box { - AnimatedVisibility(eventDetailUiState.state.isLoading) { + AnimatedVisibility(model.state.isLoading) { CircularProgressIndicator() } when { - eventDetailUiState.state.isSuccess -> { - Text(text = "${eventDetailUiState.detail}") + model.state.isSuccess -> { + Text(text = "${model.detail}") } - eventDetailUiState.state.isFail -> { + model.state.isFail -> { Row { Text("gagal loading nih, refresh yuk") Button( diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt index 18d39c1..85c6d43 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt @@ -4,37 +4,49 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember 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.AppEvent import id.gdg.app.ui.screen.content.PreviousEventContent import id.gdg.app.ui.screen.content.UpcomingEventContent -import id.gdg.app.ui.state.ChapterUiModel +import id.gdg.app.ui.state.AppUiModel +import id.gdg.app.ui.state.EventChapterUiModel +import id.gdg.app.ui.state.EventDetailUiModel import id.gdg.ui.TwoPanelScaffold import id.gdg.ui.TwoPanelScaffoldAnimationSpec import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowSizeClass import id.gdg.ui.androidx.compose.material3.windowsizeclass.CommonWindowWidthSizeClass import id.gdg.ui.androidx.compose.material3.windowsizeclass.LocalWindowSizeClass +import kotlinx.coroutines.delay @Composable fun MainScreen( - viewModel: AppViewModel, + uiState: AppUiModel, + detailUiState: EventDetailUiModel, + onSendEvent: (AppEvent) -> Unit, navigateToDetailScreen: (String) -> Unit ) { - val chapterUiState by viewModel.chapterUiState.collectAsState() - var selectedEventId by rememberSaveable { mutableStateOf("") } - + var selectedChapterId by remember { mutableStateOf(uiState.chapterUiState.selectedChapterId) } val windowSizeClazz: CommonWindowSizeClass = LocalWindowSizeClass.current + var shouldPanelOpened: Boolean? by rememberSaveable { mutableStateOf(null) } var panelVisibility by rememberSaveable { mutableStateOf(shouldPanelOpened != null) } + var selectedEventId by rememberSaveable { mutableStateOf("") } + + LaunchedEffect(selectedChapterId) { + onSendEvent(AppEvent.FetchPreviousEvent(selectedChapterId)) + onSendEvent(AppEvent.FetchUpcomingEvent(selectedChapterId)) + } LaunchedEffect(Unit) { - viewModel.sendEvent(AppEvent.InitialContent) + delay(3_000) + + selectedChapterId = 1053 + onSendEvent(AppEvent.ChangeChapterId(selectedChapterId)) } LaunchedEffect(windowSizeClazz) { @@ -50,7 +62,7 @@ fun MainScreen( ), body = { MainScreenContent( - chapterUiState = chapterUiState, + chapterUiState = uiState.eventUiState, onEventDetailClicked = { // If the screen size is compact (or mobile device screen size), then // navigate to detail page with router. Otherwise, render the [panel]. @@ -64,7 +76,7 @@ fun MainScreen( panelVisibility = true }, onRefreshPreviousContentClicked = { - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + onSendEvent(AppEvent.FetchPreviousEvent(selectedChapterId)) } ) }, @@ -72,8 +84,9 @@ fun MainScreen( Surface(tonalElevation = 1.dp) { if (shouldPanelOpened != null) { EventDetailScreen( - viewModel = viewModel, - eventId = selectedEventId + model = detailUiState, + eventId = selectedEventId, + onSendEvent = onSendEvent ) } } @@ -83,7 +96,7 @@ fun MainScreen( @Composable fun MainScreenContent( - chapterUiState: ChapterUiModel, + chapterUiState: EventChapterUiModel, onEventDetailClicked: (String) -> Unit, onRefreshPreviousContentClicked: () -> Unit ) { diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt index 3cc0682..975fc13 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt @@ -39,13 +39,20 @@ fun PreviousEventContent( ) { item { HeadlineSection("Previous Events") } - items(data.toEventContent()) { - EventSimpleCard(it) { eventId -> + items(data.data) { + EventSimpleCard( + id = it.id.toString(), + bannerUrl = it.eventImageUrl, + eventName = it.title, + date = it.startDate, + type = it.audienceType.toString(), + ) { eventId -> onEventClicked(eventId) } } } } + data.state.isFail -> { Row { Text("gagal loading nih, refresh yuk") 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/screen/uimodel/EventContent.mapper.kt deleted file mode 100644 index 21def42..0000000 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package id.gdg.app.ui.screen.uimodel - -import id.gdg.event.model.EventModel -import id.gdg.ui.component.EventContent - -fun EventModel.toEventContent(): EventContent { - return EventContent( - id = "$id", - bannerUrl = eventImageUrl, - eventName = title, - date = startDate, - type = audienceType.toString() - ) -} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/AppUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/AppUiModel.kt new file mode 100644 index 0000000..90e1999 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/AppUiModel.kt @@ -0,0 +1,6 @@ +package id.gdg.app.ui.state + +data class AppUiModel( + val eventUiState: EventChapterUiModel = EventChapterUiModel.Default, + val chapterUiState: ChapterUiModel = ChapterUiModel.Default +) \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt index 2eef368..e3b4968 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt @@ -1,17 +1,16 @@ package id.gdg.app.ui.state -import id.gdg.app.ui.state.partial.PreviousEventsUiModel -import id.gdg.app.ui.state.partial.UpcomingEventUiModel +import id.gdg.chapter.model.ChapterModel data class ChapterUiModel( - val upcomingEvent: UpcomingEventUiModel, - val previousEvents: PreviousEventsUiModel + val selectedChapterId: Int, + val chapters: List ) { companion object { val Default get() = ChapterUiModel( - upcomingEvent = UpcomingEventUiModel.Empty, - previousEvents = PreviousEventsUiModel.Empty + selectedChapterId = -1, + chapters = listOf() ) } -} +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventChapterUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventChapterUiModel.kt new file mode 100644 index 0000000..56d870e --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventChapterUiModel.kt @@ -0,0 +1,17 @@ +package id.gdg.app.ui.state + +import id.gdg.app.ui.state.partial.PreviousEventsUiModel +import id.gdg.app.ui.state.partial.UpcomingEventUiModel + +data class EventChapterUiModel( + val upcomingEvent: UpcomingEventUiModel, + val previousEvents: PreviousEventsUiModel +) { + + companion object { + val Default get() = EventChapterUiModel( + upcomingEvent = UpcomingEventUiModel.Empty, + previousEvents = PreviousEventsUiModel.Empty + ) + } +} diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt index 5afac40..d8b4a03 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/EventDetailUiModel.kt @@ -1,6 +1,6 @@ package id.gdg.app.ui.state -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/state/partial/PreviousEventsUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt index 41b80de..35ed9cd 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/PreviousEventsUiModel.kt @@ -1,20 +1,17 @@ package id.gdg.app.ui.state.partial -import id.gdg.app.ui.screen.uimodel.toEventContent -import id.gdg.app.ui.state.common.UiState +import id.gdg.app.common.UiState import id.gdg.event.model.EventModel data class PreviousEventsUiModel( val state: UiState, - val previousEvents: List, + val data: List, ) { - fun toEventContent() = previousEvents.map { it.toEventContent() } - companion object { val Empty get() = PreviousEventsUiModel( state = UiState.Loading, - previousEvents = listOf() + data = listOf() ) } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt index a80dbe8..2f1fb2a 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/partial/UpcomingEventUiModel.kt @@ -1,17 +1,17 @@ package id.gdg.app.ui.state.partial -import id.gdg.app.ui.state.common.UiState +import id.gdg.app.common.UiState import id.gdg.event.model.EventModel data class UpcomingEventUiModel( val state: UiState, - val upcomingEvent: EventModel?, + val data: EventModel?, ) { companion object { val Empty get() = UpcomingEventUiModel( state = UiState.Loading, - upcomingEvent = null + data = null ) } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/update/Chapter.update.kt b/app/src/commonMain/kotlin/id/gdg/app/update/Chapter.update.kt new file mode 100644 index 0000000..90d67cc --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/update/Chapter.update.kt @@ -0,0 +1,47 @@ +package id.gdg.app.update + +import id.gdg.app.common.Update +import id.gdg.app.common.UpdateScope +import id.gdg.app.ui.AppEvent +import id.gdg.app.ui.state.ChapterUiModel +import id.gdg.chapter.domain.GetChapterIdUseCase +import id.gdg.chapter.domain.GetChapterListUseCase +import id.gdg.chapter.domain.SetChapterIdUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +interface ChapterUpdate : Update { + + val chapterUiState: Flow +} + +class ChapterUpdateImpl( + private val chapterListUseCase: GetChapterListUseCase, + private val getCurrentChapterUseCase: GetChapterIdUseCase, + private val setCurrentChapterUseCase: SetChapterIdUseCase, + private val updateScope: UpdateScope, +) : ChapterUpdate, UpdateScope by updateScope { + + override val chapterUiState: Flow + get() = getCurrentChapterUseCase().map { + ChapterUiModel( + selectedChapterId = it ?: Int.MIN_VALUE, + chapters = chapterListUseCase() + ) + }.flowOn(Dispatchers.Default) + + override fun handleEvent(event: AppEvent) { + if (event is AppEvent.ChangeChapterId) { + shouldChangeCurrentChapterId(event.chapterId) + } + } + + private fun shouldChangeCurrentChapterId(chapterId: Int) { + scope.launch { + setCurrentChapterUseCase(chapterId) + } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/update/ChapterEvent.update.kt b/app/src/commonMain/kotlin/id/gdg/app/update/ChapterEvent.update.kt new file mode 100644 index 0000000..e86b8a0 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/update/ChapterEvent.update.kt @@ -0,0 +1,80 @@ +package id.gdg.app.update + +import id.gdg.app.common.Update +import id.gdg.app.common.UpdateScope +import id.gdg.app.common.UiState +import id.gdg.app.common.asUiState +import id.gdg.app.ui.AppEvent +import id.gdg.app.ui.state.EventChapterUiModel +import id.gdg.app.ui.state.partial.PreviousEventsUiModel +import id.gdg.app.ui.state.partial.UpcomingEventUiModel +import id.gdg.event.domain.GetPreviousEventUseCase +import id.gdg.event.domain.GetUpcomingEventUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +interface ChapterEventUpdate : Update { + + val eventsUiState: Flow +} + +class ChapterEventUpdateImpl( + private val upcomingEventUseCase: GetUpcomingEventUseCase, + private val previousEventUseCase: GetPreviousEventUseCase, + private val updateScope: UpdateScope +) : ChapterEventUpdate, UpdateScope by updateScope { + + private var _upcomingEvent = MutableStateFlow(UpcomingEventUiModel.Empty) + private var _previousEvents = MutableStateFlow(PreviousEventsUiModel.Empty) + + override val eventsUiState: Flow + get() = combine( + _upcomingEvent, + _previousEvents + ) { upcoming, previous -> + EventChapterUiModel(upcoming, previous) + }.flowOn(Dispatchers.Default) + + override fun handleEvent(event: AppEvent) { + when(event) { + is AppEvent.FetchPreviousEvent -> fetchPreviousEvent(event.chapterId) + is AppEvent.FetchUpcomingEvent -> fetchUpcomingEvents(event.chapterId) + else -> Unit + } + } + + private fun fetchPreviousEvent(chapterId: Int) { + _previousEvents.update { it.copy(state = UiState.Loading) } + + scope.launch { + val result = previousEventUseCase(chapterId) + + _previousEvents.update { + it.copy( + state = result.asUiState(), + data = result.getOrNull() ?: emptyList() + ) + } + } + } + + private fun fetchUpcomingEvents(chapterId: Int) { + _upcomingEvent.update { it.copy(state = UiState.Loading) } + + scope.launch { + val result = upcomingEventUseCase(chapterId) + + _upcomingEvent.update { + it.copy( + state = result.asUiState(), + data = result.getOrNull() + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/update/EventDetail.update.kt b/app/src/commonMain/kotlin/id/gdg/app/update/EventDetail.update.kt new file mode 100644 index 0000000..37c44b4 --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/update/EventDetail.update.kt @@ -0,0 +1,55 @@ +package id.gdg.app.update + +import id.gdg.app.common.Update +import id.gdg.app.common.UpdateScope +import id.gdg.app.common.UiState +import id.gdg.app.common.asUiState +import id.gdg.app.ui.AppEvent +import id.gdg.app.ui.state.EventDetailUiModel +import id.gdg.event.domain.GetEventDetailUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface EventDetailUpdate : Update { + + val eventDetailUiState: Flow +} + +class EventDetailUpdateImpl( + private val eventDetailUseCase: GetEventDetailUseCase, + private val updateScope: UpdateScope, +) : EventDetailUpdate, UpdateScope by updateScope { + + private var _eventDetailUiState = MutableStateFlow(EventDetailUiModel.Empty) + + override val eventDetailUiState: Flow + get() = _eventDetailUiState.asStateFlow() + + override fun handleEvent(event: AppEvent) { + if (event is AppEvent.EventDetail) { + fetchEventDetail(event.eventId) + } + } + + private fun fetchEventDetail(eventId: Int) { + _eventDetailUiState.update { it.copy(state = UiState.Loading) } + + scope.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/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt b/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt index 39b1713..b6143f2 100644 --- a/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt +++ b/app/src/commonTest/kotlin/id/gdg/app/AppViewModelTest.kt @@ -1,12 +1,9 @@ package id.gdg.app import app.cash.turbine.test +import id.gdg.app.common.UiState 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.event.model.EventDetailModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,7 +39,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(AppEvent. ChangeChapterId(expectedValue)) runBlocking { robot.getCurrentChapterUseCase().test { @@ -51,46 +48,18 @@ class AppViewModelTest : KoinTest { } } - @Test - fun `when InitialContent is invoked then upcoming and previous events are returned`() { - val events = robot.createEvents() - - robot.upcomingEventUseCase.setData(Result.success(events.first())) - robot.previousEventUseCase.setData(Result.success(events)) - - val expectedValue = ChapterUiModel( - upcomingEvent = UpcomingEventUiModel( - state = UiState.Success, - upcomingEvent = events.first() - ), - previousEvents = PreviousEventsUiModel( - state = UiState.Success, - previousEvents = events - ) - ) - - viewModel.sendEvent(AppEvent.ChangeChapterId(1)) - viewModel.sendEvent(AppEvent.InitialContent) - - runBlocking { - viewModel.chapterUiState.test { - assertTrue { expectMostRecentItem() == expectedValue } - } - } - } - @Test fun `when FetchPreviousEvent is invoked then the top three previous events are returned`() { val events = robot.createEvents() robot.previousEventUseCase.setData(Result.success(events)) - viewModel.sendEvent(AppEvent.FetchPreviousEvent) + viewModel.sendEvent(AppEvent. FetchPreviousEvent(1)) runBlocking { - viewModel.chapterUiState.test { + viewModel.uiState.test { val actualValue = expectMostRecentItem() - assertTrue { actualValue.previousEvents.state is UiState.Success } - assertTrue { actualValue.previousEvents.previousEvents.size == 3 } + assertTrue { actualValue.eventUiState.previousEvents.state is UiState.Success } + assertTrue { actualValue.eventUiState.previousEvents.data.size == 3 } } } } @@ -99,13 +68,13 @@ 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(AppEvent. FetchPreviousEvent(1)) runBlocking { - viewModel.chapterUiState.test { + viewModel.uiState.test { val actualValue = expectMostRecentItem() - assertTrue { actualValue.previousEvents.state is UiState.Success } - assertTrue { actualValue.previousEvents.previousEvents.isEmpty() } + assertTrue { actualValue.eventUiState.previousEvents.state is UiState.Success } + assertTrue { actualValue.eventUiState.previousEvents.data.isEmpty() } } } } @@ -114,11 +83,11 @@ 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(AppEvent. FetchPreviousEvent(1)) runBlocking { - viewModel.chapterUiState.test { - assertTrue { expectMostRecentItem().previousEvents.state is UiState.Fail } + viewModel.uiState.test { + assertTrue { expectMostRecentItem().eventUiState.previousEvents.state is UiState.Fail } } } } @@ -128,13 +97,13 @@ class AppViewModelTest : KoinTest { val events = robot.createEvents() robot.upcomingEventUseCase.setData(Result.success(events.first())) - viewModel.sendEvent(AppEvent.FetchUpcomingEvent) + viewModel.sendEvent(AppEvent. FetchUpcomingEvent(1)) runBlocking { - viewModel.chapterUiState.test { + viewModel.uiState.test { val actualValue = expectMostRecentItem() - assertTrue { actualValue.upcomingEvent.state is UiState.Success } - assertTrue { actualValue.upcomingEvent.upcomingEvent != null } + assertTrue { actualValue.eventUiState.upcomingEvent.state is UiState.Success } + assertTrue { actualValue.eventUiState.upcomingEvent.data != null } } } } @@ -143,13 +112,13 @@ 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(AppEvent. FetchUpcomingEvent(0)) runBlocking { - viewModel.chapterUiState.test { + viewModel.uiState.test { val actualValue = expectMostRecentItem() - assertTrue { actualValue.upcomingEvent.state is UiState.Success } - assertTrue { actualValue.upcomingEvent.upcomingEvent == null } + assertTrue { actualValue.eventUiState.upcomingEvent.state is UiState.Success } + assertTrue { actualValue.eventUiState.upcomingEvent.data == null } } } } @@ -158,11 +127,11 @@ 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(AppEvent. FetchUpcomingEvent(0)) runBlocking { - viewModel.chapterUiState.test { - assertTrue { expectMostRecentItem().upcomingEvent.state is UiState.Fail } + viewModel.uiState.test { + assertTrue { expectMostRecentItem().eventUiState.upcomingEvent.state is UiState.Fail } } } } @@ -175,7 +144,7 @@ class AppViewModelTest : KoinTest { robot.eventDetailUseCase.setData(Result.success(expectedValue)) - viewModel.sendEvent(AppEvent.EventDetail(1)) + viewModel.sendEvent(AppEvent. EventDetail(1)) runBlocking { viewModel.eventDetailUiState.test { @@ -190,7 +159,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(AppEvent. EventDetail(-1)) 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..e88f223 100644 --- a/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt +++ b/app/src/commonTest/kotlin/id/gdg/app/robot/AppViewModelRobot.kt @@ -4,7 +4,10 @@ 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.di.appModule +import id.gdg.app.common.UpdateScope +import id.gdg.app.update.ChapterEventUpdateImpl +import id.gdg.app.update.ChapterUpdateImpl +import id.gdg.app.update.EventDetailUpdateImpl import id.gdg.app.stub.ChapterSelectionLocalStoreStub import id.gdg.app.stub.GetChapterListUseCaseStub import id.gdg.app.stub.GetEventDetailUseCaseStub @@ -14,6 +17,7 @@ import id.gdg.chapter.domain.GetChapterIdUseCaseImpl import id.gdg.chapter.domain.SetChapterIdUseCaseImpl import id.gdg.event.domain.mapper.AudienceType import id.gdg.event.model.EventModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -29,7 +33,7 @@ import org.koin.test.KoinTest object AppViewModelRobot : KoinTest { val testDispatcher = UnconfinedTestDispatcher() - val testScope = TestScope(testDispatcher + Job()) + private val testScope = TestScope(testDispatcher + Job()) /** * As the mockK haven't support for the Kotlin 2.0 yet, @@ -46,13 +50,44 @@ object AppViewModelRobot : KoinTest { val getCurrentChapterUseCase by lazy { GetChapterIdUseCaseImpl(localStore) } private val setCurrentChapterUseCase by lazy { SetChapterIdUseCaseImpl(localStore) } + private val testUpdateScope by lazy { + object : UpdateScope { + override val scope: CoroutineScope + get() = testScope + + override fun shouldUseViewModelScope(scope: CoroutineScope) = Unit + override fun close() = Unit + } + } + + private val chapterUpdate by lazy { + ChapterUpdateImpl( + chapterListUseCase, + getCurrentChapterUseCase, + setCurrentChapterUseCase, + testUpdateScope + ) + } + + private val chapterEventUpdate by lazy { + ChapterEventUpdateImpl( + upcomingEventUseCase, + previousEventUseCase, + testUpdateScope + ) + } + + private val eventDetailUpdate by lazy { + EventDetailUpdateImpl( + eventDetailUseCase, + testUpdateScope + ) + } + fun createViewModel() = AppViewModel( - chapterListUseCase, - getCurrentChapterUseCase, - setCurrentChapterUseCase, - upcomingEventUseCase, - previousEventUseCase, - eventDetailUseCase + chapterUpdate, + chapterEventUpdate, + eventDetailUpdate ) fun setUp() { @@ -68,7 +103,7 @@ object AppViewModelRobot : KoinTest { } startKoin { - modules(appModule + dataStoreModule) + modules(dataStoreModule) } } diff --git a/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt b/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt index 972b223..49f04ab 100644 --- a/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt +++ b/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt @@ -22,26 +22,22 @@ import com.seiko.imageloader.model.ImageAction import com.seiko.imageloader.rememberImageSuccessPainter import com.seiko.imageloader.ui.AutoSizeBox -data class EventContent( - val id: String, - val bannerUrl: String, - val eventName: String, - val date: String, - val type: String -) - @Composable fun EventSimpleCard( - content: EventContent, + id: String, + bannerUrl: String, + eventName: String, + date: String, + type: String, navigateToEvent: (String) -> Unit ) { Row( modifier = Modifier - .clickable(onClick = { navigateToEvent(content.id) }) + .clickable(onClick = { navigateToEvent(id) }) .padding(4.dp) ) { EventBannerImage( - url = content.bannerUrl, + url = bannerUrl, modifier = Modifier .padding(16.dp) ) @@ -50,8 +46,8 @@ fun EventSimpleCard( .weight(1f) .padding(vertical = 10.dp) ) { - EventTitle(content.eventName) - DateAndAudienceType(content.date, content.type) + EventTitle(eventName) + DateAndAudienceType(date, type) } } }