diff --git a/app-ios/Sources/ContributorFeature/ContributorView.swift b/app-ios/Sources/ContributorFeature/ContributorView.swift index 9fb614844..6a6859db6 100644 --- a/app-ios/Sources/ContributorFeature/ContributorView.swift +++ b/app-ios/Sources/ContributorFeature/ContributorView.swift @@ -29,6 +29,7 @@ public let contributorReducer = Reducer { s switch action { case .refresh: return .run { @MainActor subscriber in + try await environment.staffRepository.refresh() for try await result: [Staff] in environment.staffRepository.staff().stream() { await subscriber.send( .refreshResponse( diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/DataContributorsRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/DataContributorsRepository.kt index 87d77f3d0..de88be1ec 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/DataContributorsRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/DataContributorsRepository.kt @@ -3,27 +3,30 @@ package io.github.droidkaigi.confsched2022.data.contributors import io.github.droidkaigi.confsched2022.model.Contributor import io.github.droidkaigi.confsched2022.model.ContributorsRepository import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow // TODO: Move to core-testing, once contributors server is created public class DataContributorsRepository( private val contributorsApi: ContributorsApi, ) : ContributorsRepository { + private val contributorsStateFlow = + MutableStateFlow>(persistentListOf()) + override fun contributors(): Flow> { return callbackFlow { - send( - contributorsApi - .contributors() - .toPersistentList() - ) - awaitClose { } + contributorsStateFlow.collect { + send(it) + } } } override suspend fun refresh() { - TODO("Not yet implemented") + contributorsStateFlow.value = contributorsApi + .contributors() + .toPersistentList() } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/FakeContributorsRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/FakeContributorsRepository.kt index 4b6b33579..9824f1236 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/FakeContributorsRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/contributors/FakeContributorsRepository.kt @@ -14,6 +14,5 @@ public class FakeContributorsRepository : ContributorsRepository { } override suspend fun refresh() { - TODO("Not yet implemented") } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/DataSponsorsRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/DataSponsorsRepository.kt index f0acea5a1..a95e36be9 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/DataSponsorsRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/DataSponsorsRepository.kt @@ -3,18 +3,25 @@ package io.github.droidkaigi.confsched2022.data.sponsors import io.github.droidkaigi.confsched2022.model.Sponsor import io.github.droidkaigi.confsched2022.model.SponsorsRepository import kotlinx.collections.immutable.PersistentList -import kotlinx.coroutines.channels.awaitClose +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow public class DataSponsorsRepository( private val sponsorsApi: SponsorsApi ) : SponsorsRepository { + private val sponsorsStateFlow = + MutableStateFlow>(persistentListOf()) override fun sponsors(): Flow> = callbackFlow { - send( - sponsorsApi.sponsors() - ) - awaitClose { } + sponsorsStateFlow.collect { + send(it) + } } + + override suspend fun refresh() { + sponsorsStateFlow.value = sponsorsApi + .sponsors() + } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/FakeSponsorsRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/FakeSponsorsRepository.kt index 6ef626c98..02655f1dd 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/FakeSponsorsRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/sponsors/FakeSponsorsRepository.kt @@ -9,4 +9,7 @@ import kotlinx.coroutines.flow.flowOf public class FakeSponsorsRepository : SponsorsRepository { override fun sponsors(): Flow> = flowOf(Sponsor.fakes()) + + override suspend fun refresh() { + } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/DataStaffRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/DataStaffRepository.kt index 3a3d37499..b45bdaa07 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/DataStaffRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/DataStaffRepository.kt @@ -3,22 +3,29 @@ package io.github.droidkaigi.confsched2022.data.staff import io.github.droidkaigi.confsched2022.model.Staff import io.github.droidkaigi.confsched2022.model.StaffRepository import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow public class DataStaffRepository( private val staffApi: StaffApi ) : StaffRepository { + private val staffStateFlow = + MutableStateFlow>(persistentListOf()) + override fun staff(): Flow> { return callbackFlow { - send( - staffApi - .staff() - .toPersistentList() - ) - awaitClose { } + staffStateFlow.collect { + send(it) + } } } + + override suspend fun refresh() { + staffStateFlow.value = staffApi + .staff() + .toPersistentList() + } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/FakeStaffRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/FakeStaffRepository.kt index dffaff3fc..d40e85ee9 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/FakeStaffRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/data/staff/FakeStaffRepository.kt @@ -12,4 +12,7 @@ public class FakeStaffRepository : StaffRepository { override fun staff(): Flow> { return flowOf(staff) } + + override suspend fun refresh() { + } } diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/SponsorsRepository.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/SponsorsRepository.kt index 1fc95c359..238451e36 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/SponsorsRepository.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/SponsorsRepository.kt @@ -5,4 +5,6 @@ import kotlinx.coroutines.flow.Flow public interface SponsorsRepository { public fun sponsors(): Flow> + + public suspend fun refresh() } diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/StaffRepository.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/StaffRepository.kt index 8e11285d3..d08e13a03 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/StaffRepository.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched2022/model/StaffRepository.kt @@ -5,4 +5,6 @@ import kotlinx.coroutines.flow.Flow public interface StaffRepository { public fun staff(): Flow> + + public suspend fun refresh() } diff --git a/core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/AppErrorSnackbarEffect.kt b/core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/common/AppErrorSnackbarEffect.kt similarity index 94% rename from core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/AppErrorSnackbarEffect.kt rename to core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/common/AppErrorSnackbarEffect.kt index 3d097df68..541924d60 100644 --- a/core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/AppErrorSnackbarEffect.kt +++ b/core/ui/src/main/java/io/github/droidkaigi/confsched2022/feature/common/AppErrorSnackbarEffect.kt @@ -1,4 +1,4 @@ -package io.github.droidkaigi.confsched2022.feature.announcement +package io.github.droidkaigi.confsched2022.feature.common import androidx.compose.material3.SnackbarDuration.Long import androidx.compose.material3.SnackbarHostState diff --git a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/Announcements.kt b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/Announcements.kt index f16bea6e1..631ea8daa 100644 --- a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/Announcements.kt +++ b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2022/feature/announcement/Announcements.kt @@ -40,6 +40,7 @@ import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTopAppBar import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiColors import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.AnnouncementsByDate import io.github.droidkaigi.confsched2022.model.fakes import io.github.droidkaigi.confsched2022.strings.Strings diff --git a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/Contributors.kt b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/Contributors.kt index 082af67d7..a1517c0c3 100644 --- a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/Contributors.kt +++ b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/Contributors.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -19,6 +21,7 @@ import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTopAppBar import io.github.droidkaigi.confsched2022.designsystem.components.UsernameRow import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.Contributor import io.github.droidkaigi.confsched2022.model.fakes import io.github.droidkaigi.confsched2022.strings.Strings @@ -38,6 +41,8 @@ fun ContributorsScreenRoot( uiModel = uiModel, showNavigationIcon = showNavigationIcon, onNavigationIconClick = onNavigationIconClick, + onRetryButtonClick = { viewModel.onRetryButtonClick() }, + onAppErrorNotified = { viewModel.onAppErrorNotified() }, onLinkClick = onLinkClick ) } @@ -47,10 +52,15 @@ fun Contributors( uiModel: ContributorsUiModel, showNavigationIcon: Boolean, onNavigationIconClick: () -> Unit, + onRetryButtonClick: () -> Unit, + onAppErrorNotified: () -> Unit, onLinkClick: (url: String, packageName: String?) -> Unit, modifier: Modifier = Modifier, ) { + val snackbarHostState = remember { SnackbarHostState() } + KaigiScaffold( + snackbarHostState = snackbarHostState, modifier = modifier, topBar = { KaigiTopAppBar( @@ -64,9 +74,17 @@ fun Contributors( ) } ) { innerPadding -> + AppErrorSnackbarEffect( + appError = uiModel.appError, + snackBarHostState = snackbarHostState, + onAppErrorNotified = onAppErrorNotified, + onRetryButtonClick = onRetryButtonClick + ) Box { when (uiModel.state) { - is Error -> TODO() + is Error -> { + // Do nothing + } Loading -> Box( modifier = Modifier.padding(innerPadding).fillMaxSize(), contentAlignment = Alignment.Center, @@ -103,10 +121,13 @@ fun ContributorsPreview() { uiModel = ContributorsUiModel( state = Success( Contributor.fakes() - ) + ), + appError = null, ), showNavigationIcon = true, onNavigationIconClick = {}, + onRetryButtonClick = {}, + onAppErrorNotified = {}, onLinkClick = { _, _ -> }, ) } diff --git a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsUiModel.kt b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsUiModel.kt index a07ddd2d7..6d167e09d 100644 --- a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsUiModel.kt +++ b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsUiModel.kt @@ -1,7 +1,11 @@ package io.github.droidkaigi.confsched2022.feature.contributors +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.Contributor import io.github.droidkaigi.confsched2022.ui.UiLoadState import kotlinx.collections.immutable.PersistentList -data class ContributorsUiModel(val state: UiLoadState>) +data class ContributorsUiModel( + val state: UiLoadState>, + val appError: AppError? +) diff --git a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsViewModel.kt b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsViewModel.kt index 86aab043a..0460b5045 100644 --- a/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsViewModel.kt +++ b/feature/contributors/src/main/java/io/github/droidkaigi/confsched2022/feature/contributors/ContributorsViewModel.kt @@ -3,33 +3,64 @@ package io.github.droidkaigi.confsched2022.feature.contributors import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock.ContextClock import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.ContributorsRepository import io.github.droidkaigi.confsched2022.ui.UiLoadState import io.github.droidkaigi.confsched2022.ui.asLoadState import io.github.droidkaigi.confsched2022.ui.moleculeComposeState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ContributorsViewModel @Inject constructor( - contributorsRepository: ContributorsRepository, + private val contributorsRepository: ContributorsRepository, ) : ViewModel() { private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - val uiModel: State + private val contributorsFlow = contributorsRepository + .contributors() + .asLoadState() + + private var appError by mutableStateOf(null) + + val uiModel: State = moleculeScope.moleculeComposeState( + clock = ContextClock + ) { + val contributorLoadState by contributorsFlow.collectAsState(initial = UiLoadState.Loading) + ContributorsUiModel( + state = contributorLoadState, + appError = appError + ) + } init { - val dataFlow = contributorsRepository.contributors().asLoadState() + refresh() + } - uiModel = moleculeScope.moleculeComposeState(clock = ContextClock) { - val data by dataFlow.collectAsState(initial = UiLoadState.Loading) - ContributorsUiModel(data) + fun onRetryButtonClick() { + refresh() + } + + private fun refresh() { + viewModelScope.launch { + try { + contributorsRepository.refresh() + } catch (e: AppError) { + appError = e + } } } + + fun onAppErrorNotified() { + appError = null + } } diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Search.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Search.kt index 3e7ff0b99..c6f1d90cd 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Search.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Search.kt @@ -36,6 +36,7 @@ import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar @@ -70,6 +71,7 @@ import dev.icerock.moko.resources.compose.stringResource import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme import io.github.droidkaigi.confsched2022.designsystem.theme.Typography +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day import io.github.droidkaigi.confsched2022.model.DroidKaigiSchedule import io.github.droidkaigi.confsched2022.model.Filters @@ -167,6 +169,8 @@ fun SearchRoot( viewModel.onFilterFavoritesToggle() }, onBackIconClick = onBackIconClick, + onRetryButtonClick = { viewModel.onRetryButtonClick() }, + onAppErrorNotified = { viewModel.onAppErrorNotified() }, onSearchTextAreaClicked = { viewModel.onSearchTextAreaClicked() } @@ -184,10 +188,15 @@ private fun SearchScreen( onFavoritesToggleClicked: () -> Unit, onSearchTextAreaClicked: () -> Unit, onBackIconClick: () -> Unit, + onRetryButtonClick: () -> Unit, + onAppErrorNotified: () -> Unit, modifier: Modifier = Modifier, ) { val searchWord = rememberSaveable { mutableStateOf("") } + val snackbarHostState = remember { SnackbarHostState() } + KaigiScaffold( + snackbarHostState = snackbarHostState, modifier = modifier, topBar = { if (uiModel.state is Success) { @@ -200,11 +209,19 @@ private fun SearchScreen( } }, content = { + AppErrorSnackbarEffect( + appError = uiModel.appError, + snackBarHostState = snackbarHostState, + onAppErrorNotified = onAppErrorNotified, + onRetryButtonClick = onRetryButtonClick + ) Column( modifier = Modifier.padding(paddingValues = it) ) { when (uiModel.state) { - is Error -> TODO() + is Error -> { + // Do nothing + } is Success -> { SearchFilter( modifier = Modifier @@ -545,7 +562,8 @@ fun SearchScreenPreview() { uiModel = SearchUiModel( filter = SearchFilterUiModel(), filterSheetState = SearchFilterSheetState.Hide, - state = Success(DroidKaigiSchedule.fake()) + state = Success(DroidKaigiSchedule.fake()), + appError = null, ), onItemClick = {}, onBookMarkClick = { _, _ -> }, @@ -553,6 +571,8 @@ fun SearchScreenPreview() { onFavoritesToggleClicked = {}, onDayFilterClicked = {}, onCategoriesFilteredClicked = {}, + onRetryButtonClick = {}, + onAppErrorNotified = {}, onSearchTextAreaClicked = {}, ) } @@ -566,7 +586,8 @@ fun SearchEmptyScreenPreview() { uiModel = SearchUiModel( filter = SearchFilterUiModel(), filterSheetState = SearchFilterSheetState.Hide, - state = Success(DroidKaigiSchedule.empty()) + state = Success(DroidKaigiSchedule.empty()), + appError = null, ), onItemClick = {}, onBookMarkClick = { _, _ -> }, @@ -575,6 +596,8 @@ fun SearchEmptyScreenPreview() { onDayFilterClicked = {}, onCategoriesFilteredClicked = {}, onSearchTextAreaClicked = {}, + onAppErrorNotified = {}, + onRetryButtonClick = {} ) } } diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchUiModel.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchUiModel.kt index efee70234..9e1c0c284 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchUiModel.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchUiModel.kt @@ -1,5 +1,6 @@ package io.github.droidkaigi.confsched2022.feature.sessions +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day import io.github.droidkaigi.confsched2022.model.DroidKaigiSchedule import io.github.droidkaigi.confsched2022.model.TimetableCategory @@ -8,7 +9,8 @@ import io.github.droidkaigi.confsched2022.ui.UiLoadState data class SearchUiModel( val filter: SearchFilterUiModel, val filterSheetState: SearchFilterSheetState, - val state: UiLoadState + val state: UiLoadState, + val appError: AppError? ) data class SearchFilterUiModel( diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchViewModel.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchViewModel.kt index e909e0c49..5278609d9 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchViewModel.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SearchViewModel.kt @@ -6,12 +6,14 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock.ContextClock import co.touchlab.kermit.Logger import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day import io.github.droidkaigi.confsched2022.model.Filters import io.github.droidkaigi.confsched2022.model.SessionsRepository @@ -32,18 +34,17 @@ class SearchViewModel @Inject constructor( private val sessionsRepository: SessionsRepository, sessionsZipline: SessionsZipline ) : ViewModel() { - private val moleculeScope = - CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - - val uiModel: State - private val filters = mutableStateOf(Filters()) private val filterSheetState = mutableStateOf( SearchFilterSheetState.Hide ) + private var appError by mutableStateOf(null) + + val uiModel: State = run { + val moleculeScope = + CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - init { val ziplineScheduleModifierFlow = sessionsZipline.timetableModifier() val sessionScheduleFlow = sessionsRepository.droidKaigiScheduleFlow() @@ -59,10 +60,8 @@ class SearchViewModel @Inject constructor( schedule } }.asLoadState() - - uiModel = moleculeScope.moleculeComposeState(clock = ContextClock) { + moleculeScope.moleculeComposeState(clock = ContextClock) { val schedule by scheduleFlow.collectAsState(initial = UiLoadState.Loading) - val filteredSchedule by remember(filters) { derivedStateOf { schedule.mapSuccess { it.filtered(filters.value) } @@ -76,7 +75,8 @@ class SearchViewModel @Inject constructor( isFavoritesOn = filters.value.filterFavorite ), filterSheetState = filterSheetState.value, - state = filteredSchedule + state = filteredSchedule, + appError = appError, ) } } @@ -142,4 +142,26 @@ class SearchViewModel @Inject constructor( fun onSearchTextAreaClicked() { onFilterSheetDismissed() } + + init { + refresh() + } + + fun onRetryButtonClick() { + refresh() + } + + private fun refresh() { + viewModelScope.launch { + try { + sessionsRepository.refresh() + } catch (e: AppError) { + appError = e + } + } + } + + fun onAppErrorNotified() { + appError = null + } } diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetail.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetail.kt index 58f10b4e5..ee7aba936 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetail.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetail.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -59,6 +60,7 @@ import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTag import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme import io.github.droidkaigi.confsched2022.designsystem.theme.TimetableItemColor +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.KaigiPlace.Prism import io.github.droidkaigi.confsched2022.model.Lang import io.github.droidkaigi.confsched2022.model.MultiLangText @@ -95,6 +97,8 @@ fun SessionDetailScreenRoot( SessionDetailScreen( modifier = modifier, uiModel = uiModel, + onRetryButtonClick = { viewModel.onRetryButtonClick() }, + onAppErrorNotified = { viewModel.onAppErrorNotified() }, onBackIconClick = onBackIconClick, onFavoriteClick = { currentFavorite -> viewModel.onFavoriteToggle(timetableItemId, currentFavorite) @@ -134,6 +138,8 @@ fun SessionDetailTopAppBar( @Composable fun SessionDetailScreen( uiModel: SessionDetailUiModel, + onRetryButtonClick: () -> Unit, + onAppErrorNotified: () -> Unit, modifier: Modifier = Modifier, onBackIconClick: () -> Unit = {}, onFavoriteClick: (Boolean) -> Unit = {}, @@ -141,10 +147,11 @@ fun SessionDetailScreen( onNavigateFloorMapClick: () -> Unit = {}, onRegisterCalendarClick: (TimetableItem) -> Unit = {}, ) { - val uiState = uiModel.state + val snackbarHostState = remember { SnackbarHostState() } KaigiScaffold( + snackbarHostState = snackbarHostState, topBar = { SessionDetailTopAppBar( onBackIconClick = onBackIconClick, @@ -165,9 +172,17 @@ fun SessionDetailScreen( } }, ) { innerPadding -> + AppErrorSnackbarEffect( + appError = uiModel.appError, + snackBarHostState = snackbarHostState, + onAppErrorNotified = onAppErrorNotified, + onRetryButtonClick = onRetryButtonClick + ) Box(modifier = Modifier.padding(innerPadding)) { when (uiState) { - is Error -> TODO() + is Error -> { + // Do nothing + } Loading -> Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() @@ -624,8 +639,11 @@ fun PreviewSessionDetailScreen() { KaigiTheme { SessionDetailScreen( uiModel = SessionDetailUiModel( - Success(TimetableItemWithFavorite.fake()) - ) + state = Success(TimetableItemWithFavorite.fake()), + appError = null, + ), + onRetryButtonClick = {}, + onAppErrorNotified = {}, ) } } diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailUiModel.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailUiModel.kt index 2d4362791..91987c209 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailUiModel.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailUiModel.kt @@ -1,6 +1,10 @@ package io.github.droidkaigi.confsched2022.feature.sessions +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.TimetableItemWithFavorite import io.github.droidkaigi.confsched2022.ui.UiLoadState -data class SessionDetailUiModel(val state: UiLoadState) +data class SessionDetailUiModel( + val state: UiLoadState, + val appError: AppError? +) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailViewModel.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailViewModel.kt index 54952434b..ad7b4d9c7 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailViewModel.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/SessionDetailViewModel.kt @@ -3,12 +3,15 @@ package io.github.droidkaigi.confsched2022.feature.sessions import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock.ContextClock import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.SessionsRepository import io.github.droidkaigi.confsched2022.model.TimetableItemId import io.github.droidkaigi.confsched2022.ui.UiLoadState @@ -30,16 +33,22 @@ class SessionDetailViewModel @Inject constructor( private val timetableItemId: TimetableItemId = TimetableItemId(requireNotNull(savedStateHandle.get("id"))) - val uiModel: State + private var appError by mutableStateOf(null) - init { - val timetableItemFlow = - sessionsRepository.timetableItemFlow(timetableItemId).asLoadState() + private val timetableItemFlow = + sessionsRepository.timetableItemFlow(timetableItemId).asLoadState() - uiModel = moleculeScope.moleculeComposeState(clock = ContextClock) { + val uiModel: State = + moleculeScope.moleculeComposeState(clock = ContextClock) { val timetableItem by timetableItemFlow.collectAsState(initial = UiLoadState.Loading) - SessionDetailUiModel(timetableItem) + SessionDetailUiModel( + state = timetableItem, + appError = appError + ) } + + init { + refresh() } fun onFavoriteToggle(sessionId: TimetableItemId, currentIsFavorite: Boolean) { @@ -49,4 +58,22 @@ class SessionDetailViewModel @Inject constructor( ) } } + + fun onRetryButtonClick() { + refresh() + } + + private fun refresh() { + viewModelScope.launch { + try { + sessionsRepository.refresh() + } catch (e: AppError) { + appError = e + } + } + } + + fun onAppErrorNotified() { + appError = null + } } diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Sessions.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Sessions.kt index b5a165ebc..25cf2a4c8 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Sessions.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2022/feature/sessions/Sessions.kt @@ -70,7 +70,7 @@ import dev.icerock.moko.resources.compose.stringResource import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTopAppBar import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme -import io.github.droidkaigi.confsched2022.feature.announcement.AppErrorSnackbarEffect +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.DroidKaigi2022Day import io.github.droidkaigi.confsched2022.model.DroidKaigiSchedule import io.github.droidkaigi.confsched2022.model.TimeLine diff --git a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/Sponsors.kt b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/Sponsors.kt index 6d4845966..995d2bdd2 100644 --- a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/Sponsors.kt +++ b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/Sponsors.kt @@ -20,9 +20,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -35,6 +37,7 @@ import com.google.accompanist.placeholder.material.shimmer import dev.icerock.moko.resources.compose.stringResource import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTopAppBar +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Gold import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Platinum import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Supporter @@ -58,6 +61,8 @@ fun SponsorsScreenRoot( uiModel = uiModel, showNavigationIcon = showNavigationIcon, onNavigationIconClick = onNavigationIconClick, + onRetryButtonClick = { viewModel.onRetryButtonClick() }, + onAppErrorNotified = { viewModel.onAppErrorNotified() }, onItemClick = onItemClick ) } @@ -67,10 +72,15 @@ fun Sponsors( uiModel: SponsorsUiModel, showNavigationIcon: Boolean, onNavigationIconClick: () -> Unit, + onRetryButtonClick: () -> Unit, + onAppErrorNotified: () -> Unit, modifier: Modifier = Modifier, onItemClick: (url: String) -> Unit = { _ -> }, ) { + val snackbarHostState = remember { SnackbarHostState() } + KaigiScaffold( + snackbarHostState = snackbarHostState, modifier = modifier, topBar = { KaigiTopAppBar( @@ -84,6 +94,12 @@ fun Sponsors( ) } ) { innerPadding -> + AppErrorSnackbarEffect( + appError = uiModel.appError, + snackBarHostState = snackbarHostState, + onAppErrorNotified = onAppErrorNotified, + onRetryButtonClick = onRetryButtonClick + ) when (uiModel.state) { Loading -> FullScreenLoading(Modifier.padding(innerPadding)) is Success -> @@ -101,7 +117,9 @@ fun Sponsors( onItemClick = onItemClick ) } - is Error -> TODO() + is Error -> { + // Do nothing + } } } } diff --git a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsUiModel.kt b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsUiModel.kt index bc31140dd..063e517ef 100644 --- a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsUiModel.kt +++ b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsUiModel.kt @@ -1,8 +1,10 @@ package io.github.droidkaigi.confsched2022.feature.sponsors +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.ui.UiLoadState import kotlinx.collections.immutable.PersistentList data class SponsorsUiModel( - val state: UiLoadState> + val state: UiLoadState>, + val appError: AppError? ) diff --git a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsViewModel.kt b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsViewModel.kt index 7ec6b4cbb..c3f81c126 100644 --- a/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsViewModel.kt +++ b/feature/sponsors/src/main/java/io/github/droidkaigi/confsched2022/feature/sponsors/SponsorsViewModel.kt @@ -3,6 +3,8 @@ package io.github.droidkaigi.confsched2022.feature.sponsors import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.AndroidUiDispatcher @@ -13,6 +15,7 @@ import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorItem.Title import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Gold import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Platinum import io.github.droidkaigi.confsched2022.feature.sponsors.SponsorPlan.Supporter +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.Plan import io.github.droidkaigi.confsched2022.model.Plan.GOLD import io.github.droidkaigi.confsched2022.model.Plan.PLATINUM @@ -26,26 +29,35 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SponsorsViewModel @Inject constructor( - sponsorsRepository: SponsorsRepository + private val sponsorsRepository: SponsorsRepository ) : ViewModel() { private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - val uiModel: State + private val sponsorsFlow = sponsorsRepository + .sponsors() + .map { it.mapToSponsorItems() } + .asLoadState() - init { - val sponsorsFlow = sponsorsRepository.sponsors() - .map { it.mapToSponsorItems() } - .asLoadState() + private var appError by mutableStateOf(null) - uiModel = moleculeScope.moleculeComposeState(clock = ContextClock) { - val sponsors by sponsorsFlow.collectAsState(initial = UiLoadState.Loading) - SponsorsUiModel(sponsors) - } + val uiModel: State = moleculeScope.moleculeComposeState( + clock = ContextClock + ) { + val sponsorLoadState by sponsorsFlow.collectAsState(initial = UiLoadState.Loading) + SponsorsUiModel( + state = sponsorLoadState, + appError = appError + ) + } + + init { + refresh() } private fun PersistentList.mapToSponsorItems(): PersistentList { @@ -87,4 +99,22 @@ class SponsorsViewModel @Inject constructor( link = link ) } + + fun onRetryButtonClick() { + refresh() + } + + private fun refresh() { + viewModelScope.launch { + try { + sponsorsRepository.refresh() + } catch (e: AppError) { + appError = e + } + } + } + + fun onAppErrorNotified() { + appError = null + } } diff --git a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/Staff.kt b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/Staff.kt index 3590e8bfd..02473ecc7 100644 --- a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/Staff.kt +++ b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/Staff.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -19,6 +21,7 @@ import io.github.droidkaigi.confsched2022.designsystem.components.KaigiScaffold import io.github.droidkaigi.confsched2022.designsystem.components.KaigiTopAppBar import io.github.droidkaigi.confsched2022.designsystem.components.UsernameRow import io.github.droidkaigi.confsched2022.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched2022.feature.common.AppErrorSnackbarEffect import io.github.droidkaigi.confsched2022.model.Staff import io.github.droidkaigi.confsched2022.model.fakes import io.github.droidkaigi.confsched2022.strings.Strings @@ -35,7 +38,15 @@ fun StaffScreenRoot( onLinkClick: (url: String, packageName: String?) -> Unit = { _, _ -> } ) { val uiModel by viewModel.uiModel - Staff(uiModel, showNavigationIcon, onNavigationIconClick, onLinkClick, modifier) + Staff( + uiModel = uiModel, + showNavigationIcon = showNavigationIcon, + onNavigationIconClick = onNavigationIconClick, + onRetryButtonClick = { viewModel.onRetryButtonClick() }, + onAppErrorNotified = { viewModel.onAppErrorNotified() }, + onLinkClick = onLinkClick, + modifier = modifier + ) } @Composable @@ -43,10 +54,15 @@ fun Staff( uiModel: StaffUiModel, showNavigationIcon: Boolean, onNavigationIconClick: () -> Unit, + onRetryButtonClick: () -> Unit, + onAppErrorNotified: () -> Unit, onLinkClick: (url: String, packageName: String?) -> Unit, modifier: Modifier = Modifier ) { + val snackbarHostState = remember { SnackbarHostState() } + KaigiScaffold( + snackbarHostState = snackbarHostState, topBar = { KaigiTopAppBar( showNavigationIcon = showNavigationIcon, @@ -59,9 +75,17 @@ fun Staff( ) } ) { innerPadding -> + AppErrorSnackbarEffect( + appError = uiModel.appError, + snackBarHostState = snackbarHostState, + onAppErrorNotified = onAppErrorNotified, + onRetryButtonClick = onRetryButtonClick + ) Box(modifier = Modifier.padding(innerPadding)) { when (uiModel.state) { - is Error -> TODO() + is Error -> { + // Do nothing + } is Loading -> Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, @@ -97,10 +121,13 @@ fun StaffPreview() { uiModel = StaffUiModel( state = Success( Staff.fakes() - ) + ), + appError = null, ), showNavigationIcon = true, onNavigationIconClick = {}, + onRetryButtonClick = {}, + onAppErrorNotified = {}, onLinkClick = { _, _ -> }, ) } diff --git a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffUiModel.kt b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffUiModel.kt index 9fb9d4e41..b8c0836fe 100644 --- a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffUiModel.kt +++ b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffUiModel.kt @@ -1,7 +1,11 @@ package io.github.droidkaigi.confsched2022.feature.staff +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.Staff import io.github.droidkaigi.confsched2022.ui.UiLoadState import kotlinx.collections.immutable.PersistentList -data class StaffUiModel(val state: UiLoadState>) +data class StaffUiModel( + val state: UiLoadState>, + val appError: AppError?, +) diff --git a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffViewModel.kt b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffViewModel.kt index 3f1e5e179..7cc69321c 100644 --- a/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffViewModel.kt +++ b/feature/staff/src/main/java/io/github/droidkaigi/confsched2022/feature/staff/StaffViewModel.kt @@ -3,33 +3,62 @@ package io.github.droidkaigi.confsched2022.feature.staff import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.AndroidUiDispatcher import app.cash.molecule.RecompositionClock.ContextClock import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.droidkaigi.confsched2022.model.AppError import io.github.droidkaigi.confsched2022.model.StaffRepository import io.github.droidkaigi.confsched2022.ui.UiLoadState.Loading import io.github.droidkaigi.confsched2022.ui.asLoadState import io.github.droidkaigi.confsched2022.ui.moleculeComposeState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class StaffViewModel @Inject constructor( - staffRepository: StaffRepository, + private val staffRepository: StaffRepository, ) : ViewModel() { private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - val uiModel: State + private val staffFlow = staffRepository + .staff() + .asLoadState() + + private var appError by mutableStateOf(null) + + val uiModel: State = moleculeScope.moleculeComposeState(clock = ContextClock) { + val staffState by staffFlow.collectAsState(initial = Loading) + StaffUiModel( + state = staffState, + appError = appError, + ) + } init { - val dataFlow = staffRepository.staff().asLoadState() + refresh() + } - uiModel = moleculeScope.moleculeComposeState(clock = ContextClock) { - val data by dataFlow.collectAsState(initial = Loading) - StaffUiModel(data) + fun onRetryButtonClick() { + refresh() + } + + private fun refresh() { + viewModelScope.launch { + try { + staffRepository.refresh() + } catch (e: AppError) { + appError = e + } } } + + fun onAppErrorNotified() { + appError = null + } }