diff --git a/desktopApp/src/jvmMain/kotlin/Main.kt b/desktopApp/src/jvmMain/kotlin/Main.kt index 9216d0e..f00e8fe 100644 --- a/desktopApp/src/jvmMain/kotlin/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/Main.kt @@ -1,4 +1,3 @@ - import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowState diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt index 1eacd3f..9b710f2 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt @@ -54,4 +54,5 @@ interface AppStrings { val permissionRequired: String val grantPermission: String val displayCurrencyDescription: String + val search: String } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt index 3a93d31..76fcff3 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt @@ -21,6 +21,7 @@ object StringEn : AppStrings { override val moreInfo = "More Info" override val aboutMe = "About me" override val whatsNew = "What's New" + override val search = "Search" // Settings Screen override val appearance = "Appearance" diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/Scaffold.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/Scaffold.kt index cf83e1e..29eee99 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/Scaffold.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/Scaffold.kt @@ -13,8 +13,8 @@ import me.sujanpoudel.playdeals.common.navigation.Navigator @Composable fun Scaffold( modifier: Modifier = Modifier, - title: String? = null, - showNavBackIcon: Boolean = true, + title: ScaffoldToolbar.ScaffoldTitle = ScaffoldToolbar.ScaffoldTitle.None, + showNavIcon: Boolean = true, navigationIcon: @Composable (Navigator) -> Unit = { it -> ScaffoldToolbar.NavigationIcon(it) }, actions: (@Composable (Navigator) -> Unit)? = null, content: @Composable BoxScope.() -> Unit, @@ -23,7 +23,7 @@ fun Scaffold( topBar = { ScaffoldToolbar( title = title, - showNavBackIcon = showNavBackIcon, + showNavIcon = showNavIcon, actions = actions, navigationIcon = navigationIcon, ) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/ScaffoldToolbar.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/ScaffoldToolbar.kt index cb21054..d435be3 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/ScaffoldToolbar.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/ScaffoldToolbar.kt @@ -1,5 +1,19 @@ package me.sujanpoudel.playdeals.common.ui.components.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.CenterAlignedTopAppBar @@ -12,14 +26,42 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import me.sujanpoudel.playdeals.common.navigation.Navigator +import me.sujanpoudel.playdeals.common.strings.Strings + +@Composable +fun rememberTextTitle(title: String): ScaffoldToolbar.ScaffoldTitle { + return remember(title) { + ScaffoldToolbar.ScaffoldTitle.TextTitle(title) + } +} object ScaffoldToolbar { + sealed class ScaffoldTitle { + data object None : ScaffoldTitle() + + @Immutable + data class TextTitle(val text: String) : ScaffoldTitle() + + @Immutable + class SearchBarTitle(val text: State, val onTextUpdated: (String) -> Unit) : ScaffoldTitle() + } + @Composable fun NavigationIcon(navigator: Navigator) { IconButton(onClick = navigator::pop) { @@ -27,39 +69,110 @@ object ScaffoldToolbar { } } - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable operator fun invoke( modifier: Modifier = Modifier, - title: String? = null, - showNavBackIcon: Boolean = true, + title: ScaffoldTitle, + showNavIcon: Boolean = true, + alwaysShowNavIcon: Boolean = false, navigationIcon: @Composable (Navigator) -> Unit = { it -> NavigationIcon(it) }, actions: (@Composable (Navigator) -> Unit)? = null, behaviour: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()), ) { val navigator = Navigator.current - CenterAlignedTopAppBar( - modifier = modifier, - title = { - title?.let { - Text( - text = title, - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), - textAlign = TextAlign.Center, - ) - } - }, - navigationIcon = { - if (showNavBackIcon && navigator.backStackCount.value > 1) { - navigationIcon(navigator) - } - }, - actions = { actions?.invoke(navigator) }, + modifier = modifier.windowInsetsPadding(WindowInsets(top = 10.dp)), + title = { ToolbarTitle(title) }, + navigationIcon = + { + if (alwaysShowNavIcon || (showNavIcon && navigator.backStackCount.value > 1)) { + navigationIcon(navigator) + } + }, + actions = + { actions?.invoke(navigator) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, ), scrollBehavior = behaviour, ) } + + @Composable + private fun ToolbarTitle(title: ScaffoldTitle) { + val contentTransform = remember { + fadeIn(animationSpec = tween(220, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(90))) + } + + AnimatedContent( + modifier = Modifier.fillMaxWidth().heightIn(min = 24.dp), + targetState = title, + transitionSpec = { contentTransform }, + ) { scaffoldTitle -> + when (scaffoldTitle) { + ScaffoldTitle.None -> {} + is ScaffoldTitle.SearchBarTitle -> SearchBarTitle(scaffoldTitle) + is ScaffoldTitle.TextTitle -> Text( + text = scaffoldTitle.text, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + textAlign = TextAlign.Center, + ) + } + } + } + + @OptIn(ExperimentalMaterialApi::class) + @Composable + private fun SearchBarTitle(title: ScaffoldTitle.SearchBarTitle) { + val interactionSource = remember { MutableInteractionSource() } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + BasicTextField( + modifier = Modifier.fillMaxWidth().height(44.dp) + .focusRequester(focusRequester), + value = title.text.value, + onValueChange = title.onTextUpdated, + interactionSource = interactionSource, + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + decorationBox = { innerTextField -> + TextFieldDefaults.OutlinedTextFieldDecorationBox( + value = title.text.value, + contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding( + top = 8.dp, + bottom = 8.dp, + ), + innerTextField = innerTextField, + placeholder = { + Text( + Strings.search, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp, + ), + ) + }, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + colors = TextFieldDefaults.outlinedTextFieldColors( + textColor = MaterialTheme.colorScheme.onSurface, + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline, + ), + ) + }, + ) + } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt index 3256be1..a013802 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt @@ -38,13 +38,15 @@ object DealContent { @Composable operator fun invoke( state: HomeScreenState, + searchTerm: String, onToggleFilterOption: (DealFilterOption) -> Unit, refreshAppDeals: () -> Unit, ) { - val deals = remember(state.allDeals, state.filterOptions) { + val deals = remember(state.allDeals, state.filterOptions, searchTerm) { state.allDeals.filterWith( filterOptions = state.filterOptions, lastUpdatedTime = state.lastUpdatedTime, + searchTerm = searchTerm, ) } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/ChangeLogScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/ChangeLogScreen.kt index 3f2937a..0d7e209 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/ChangeLogScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/ChangeLogScreen.kt @@ -37,6 +37,7 @@ import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.ChangeLog import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold +import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.resource import org.kodein.di.direct @@ -48,8 +49,8 @@ private const val CHANGELOG_PATH = "raw/changelog.md" @OptIn(ExperimentalResourceApi::class, ExperimentalFoundationApi::class) @Composable fun ChangeLogScreen() = Scaffold( - title = Strings.changelog, - showNavBackIcon = false, + title = rememberTextTitle(Strings.changelog), + showNavIcon = false, actions = { val outlineColor = MaterialTheme.colorScheme.outlineVariant IconButton( diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreen.kt index c684d78..89d58a0 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreen.kt @@ -5,7 +5,12 @@ import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar @@ -16,9 +21,12 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -33,6 +41,7 @@ import me.sujanpoudel.playdeals.common.navigation.NavTransitions import me.sujanpoudel.playdeals.common.navigation.Navigator import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar +import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar.ScaffoldTitle import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.PullRefreshIndicator import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.pullRefresh import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.rememberPullRefreshState @@ -76,11 +85,39 @@ fun HomeScreen() { val topBarState = rememberTopAppBarState() val topBarScrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState) + val searchTerm = viewModel.searchTerm.collectAsState() + var showSearchToolbar by remember { mutableStateOf(false) } + val toolbarTitle by remember { + derivedStateOf { + if (showSearchToolbar) { + ScaffoldTitle.SearchBarTitle(searchTerm, viewModel::setSearchTerm) + } else { + ScaffoldTitle.TextTitle(strings.appDeals) + } + } + } + + LaunchedEffect(showSearchToolbar) { + if (!showSearchToolbar) { + delay(100) + viewModel.setSearchTerm("") + } + } + Scaffold( topBar = { ScaffoldToolbar( - title = Strings.appDeals, + title = toolbarTitle, behaviour = topBarScrollBehaviour, + alwaysShowNavIcon = true, + navigationIcon = { + IconButton(onClick = { showSearchToolbar = showSearchToolbar.not() }) { + Icon( + imageVector = if (showSearchToolbar) Icons.Default.Clear else Icons.Default.Search, + contentDescription = "", + ) + } + }, actions = { HomeScreen.NavMenu { coroutineScope.launch { @@ -135,6 +172,7 @@ fun HomeScreen() { state, onToggleFilterOption = viewModel::toggleFilterItem, refreshAppDeals = viewModel::refreshDeals, + searchTerm = searchTerm.value, ) } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt index b600a33..f3475fa 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt @@ -23,6 +23,7 @@ data class HomeScreenState( fun List.filterWith( filterOptions: List>, lastUpdatedTime: Instant, + searchTerm: String, ): List { val selectedCategories = filterOptions .filter { it.data is DealFilterOption.Category && it.selected } @@ -38,6 +39,11 @@ fun List.filterWith( it is DealFilterOption.Category && it.value == deal.category } + val containsSearchTerm = searchTerm.isEmpty() || ( + deal.name.contains(searchTerm, ignoreCase = true) || + deal.category.contains(searchTerm, ignoreCase = true) + ) + val matchesOtherFilter = selectedOtherFilters.isEmpty() || selectedOtherFilters.all { when (it) { @@ -46,6 +52,6 @@ fun List.filterWith( DealFilterOption.NewlyAddedApps -> deal.createdAt > lastUpdatedTime } } - inSelectedCategory && matchesOtherFilter + inSelectedCategory && matchesOtherFilter && containsSearchTerm } } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt index 70505a1..965a82e 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenViewModel.kt @@ -27,12 +27,11 @@ class HomeScreenViewModel( private val forexRepository: ForexRepository, ) : ViewModel() { - private val _state = MutableStateFlow( - HomeScreenState(lastUpdatedTime = appPreferences.lastUpdatedTime.value), - ) - + private val _state = MutableStateFlow(HomeScreenState(lastUpdatedTime = appPreferences.lastUpdatedTime.value)) val state = _state as StateFlow + val searchTerm = state("") + init { observeDeals() refreshDeals() @@ -110,7 +109,9 @@ class HomeScreenViewModel( } } - fun refreshForex() = viewModelScope.launch { + fun setSearchTerm(term: String) = searchTerm.update { term } + + private fun refreshForex() = viewModelScope.launch { forexRepository.refreshRatesIfNecessary() } diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/newDeal/NewDealScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/newDeal/NewDealScreen.kt index 2081fb2..0673c6c 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/newDeal/NewDealScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/newDeal/NewDealScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold +import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle import me.sujanpoudel.playdeals.common.ui.theme.SOFT_COLOR_ALPHA import me.sujanpoudel.playdeals.common.viewModel.viewModel @@ -18,7 +19,7 @@ import me.sujanpoudel.playdeals.common.viewModel.viewModel fun NewDealScreen() { val viewModel = viewModel() Scaffold( - title = Strings.addNewDeal, + title = rememberTextTitle(Strings.addNewDeal), ) { Column( modifier = Modifier.align(Alignment.Center) diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt index 69b507c..c377196 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/SettingsScreen.kt @@ -21,6 +21,7 @@ import me.sujanpoudel.playdeals.common.pushNotification.NotificationManager import me.sujanpoudel.playdeals.common.pushNotification.current import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold +import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.AppearanceModeSetting import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.CurrencySetting import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.Footer @@ -52,7 +53,7 @@ fun SettingsScreen() { val navigator = Navigator.current val notificationManager = NotificationManager.current - Scaffold(title = Strings.settings) { + Scaffold(title = rememberTextTitle(Strings.settings)) { Column( modifier = Modifier.fillMaxSize(), ) { diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt index 1562123..251e448 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/settings/notificationSettings/NotificationSettingsScreen.kt @@ -25,6 +25,7 @@ import me.sujanpoudel.playdeals.common.PermissionStatus import me.sujanpoudel.playdeals.common.pushNotification.pushNotificationPermissionManager import me.sujanpoudel.playdeals.common.strings.Strings import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold +import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle import me.sujanpoudel.playdeals.common.ui.components.settings.SettingsScreen.SettingItem import me.sujanpoudel.playdeals.common.viewModel.viewModel @@ -47,7 +48,7 @@ fun NotificationSettingsScreen() { val permissionStatus by notificationPermissionManager.permissionState val permissionGranted by derivedStateOf { permissionStatus == PermissionStatus.Granted } - Scaffold(title = Strings.pushNotification) { + Scaffold(title = rememberTextTitle(Strings.pushNotification)) { Column( modifier = Modifier.fillMaxSize(), ) { diff --git a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/viewModel/ViewModel.kt b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/viewModel/ViewModel.kt index c8ae30d..98910f5 100644 --- a/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/viewModel/ViewModel.kt +++ b/shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/viewModel/ViewModel.kt @@ -1,5 +1,9 @@ package me.sujanpoudel.playdeals.common.viewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + @OptIn(ExperimentalStdlibApi::class) open class ViewModel() : AutoCloseable { private val bagOfTags = hashMapOf() @@ -34,4 +38,16 @@ open class ViewModel() : AutoCloseable { } } as T } + + sealed interface VMState : StateFlow + + private class VMStateImpl constructor( + private val delegate: MutableStateFlow, + ) : MutableStateFlow by delegate, VMState + + fun VMState.update(function: (T) -> T) { + (this as VMStateImpl as MutableStateFlow).update(function) + } + + protected fun state(initial: T): VMState = VMStateImpl(MutableStateFlow(initial)) }