Skip to content

Commit

Permalink
feat: create update loop
Browse files Browse the repository at this point in the history
  • Loading branch information
isfaaghyth committed Aug 31, 2024
1 parent e021f48 commit 79f13cf
Show file tree
Hide file tree
Showing 24 changed files with 496 additions and 291 deletions.
16 changes: 13 additions & 3 deletions app/src/commonMain/kotlin/id/gdg/app/AppContent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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))
},
Expand All @@ -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))
}
Expand All @@ -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
)
}
Expand Down
162 changes: 38 additions & 124 deletions app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<AppEvent>(replay = 50)

private var _upcomingEvent = MutableStateFlow(UpcomingEventUiModel.Empty)
private var _previousEvents = MutableStateFlow(PreviousEventsUiModel.Empty)

val chapterList get() = chapterListUseCase()

val chapterUiState: StateFlow<ChapterUiModel> = combine(
_upcomingEvent,
_previousEvents
) { upcoming, previous ->
ChapterUiModel(upcoming, previous)
private val chapterUpdate: ChapterUpdate,
private val chapterEventUpdate: ChapterEventUpdate,
private val eventDetailUpdate: EventDetailUpdate
) : UpdateableViewModel() {

val uiState: StateFlow<AppUiModel> = 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<AppEvent>(replay = 50)

_previousEvents.update {
it.copy(
state = result.asUiState(),
previousEvents = result.getOrNull() ?: emptyList()
)
}
}
override fun updates(): List<Update> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package id.gdg.app.ui.state.common
package id.gdg.app.common

sealed class UiState {
data object Success : UiState()
Expand Down
47 changes: 47 additions & 0 deletions app/src/commonMain/kotlin/id/gdg/app/common/Update.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
23 changes: 23 additions & 0 deletions app/src/commonMain/kotlin/id/gdg/app/common/UpdateableViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<Update>

fun setupLoop() {
updates()
.map { it as UpdateScope }
.map {
it.shouldUseViewModelScope(viewModelScope)
addCloseable(it)
}
}

fun eventHandlers(event: AppEvent) =
updates().forEach { it.handleEvent(event) }
}
30 changes: 9 additions & 21 deletions app/src/commonMain/kotlin/id/gdg/app/di/ViewModelFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
17 changes: 17 additions & 0 deletions app/src/commonMain/kotlin/id/gdg/app/di/modules.kt
Original file line number Diff line number Diff line change
@@ -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<UpdateScope> { MainUpdateScope() }
single<ChapterUpdate> { ChapterUpdateImpl(get(), get(), get(), get()) }
single<EventDetailUpdate> { EventDetailUpdateImpl(get(), get()) }
single<ChapterEventUpdate> { ChapterEventUpdateImpl(get(), get(), get()) }
}

val appModule = listOf(
updateModule,
chapterModule,
eventModule,
)
Loading

0 comments on commit 79f13cf

Please sign in to comment.