The following Code A is from the project.
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
Why can't I use Code B in the project?
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
...
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val cityDetailsResult = viewModel.cityDetails
val uiState=if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
...
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
No, you can't write it using the mutableStateOf (direct initialization not possible). In order to understand why it not possible we need to understand the use of produceState
According to documentation available here
produceState launches a coroutine scoped to the Composition that can
push values into a returned State. Use it to convert non-Compose state
into Compose state, for example bringing external subscription-driven
state such as Flow, LiveData, or RxJava into the Composition.
So basically it is compose way of converting non-Compose state to compose the state.
if you still want to use mutableStateOf you can do something like this
var uiState = remember { mutableStateOf(DetailsUIState())}
LaunchedEffect(key1 = someKey, block = {
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
})
Note: here someKey might be another variable which handles the recomposition of the state
What is wrong with this approach?
As you can see it's taking another variable someKey to recomposition. and handling it is quite tough compared to produceState
Why can't I use Code B in the project?
The problem with code B is you don't know whether the data is loaded or not while displaying the result. It's not observing the viewModel's data but its just getting the currently available data and based on that it gives the composition.
Imagine if the viewModel is getting data now you will be having UiState with isLoading = true but after some time you get data after a successful API call or error if it fails, at that time the composable function in this case DetailsScreen doesn't know about it at all unless you are observing the Ui state somewhere above this composition and causing this composition to recompose based on newState available.
But in produceState the state of the ui will automatically changed once the suspended network call completes ...
Related
I get some data from api in kotlin and save it to room. I do the saving to Room in the viewmodel of the splash screen. However, each application saves the data to the room when it is opened, I want it to save only once. In this case I tried to do something with shared preferences but I couldn't implement it. Any ideas on this or anyone who has done something similar to this before?
hear is my code
my splash screen ui
#Composable
fun SplashScreen(
navController: NavController,
viewModel : SplashScreenViewModel = hiltViewModel()
) {
val context: Context = LocalContext.current
checkDate(context,viewModel)
val scale = remember {
Animatable(0f)
}
LaunchedEffect(key1 = true) {
scale.animateTo(
targetValue = 0.3f,
animationSpec = tween(
durationMillis = 500,
easing = {
OvershootInterpolator(2f).getInterpolation(it)
}
)
)
delay(2000L)
navController.navigate(Screen.ExchangeMainScreen.route)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_currency_exchange_logo_512),
contentDescription = "Logo"
)
}
}
fun checkDate(context:Context,viewModel: SplashScreenViewModel){
val sharedPreferences = context.getSharedPreferences("date",Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
val date = Date(System.currentTimeMillis())
val millis = date.time
editor.apply{
putLong("currentDate", millis)
}.apply()
val sharedDate = sharedPreferences.getLong("currentDate",1)
println(sharedDate)
val isDayPassed = (System.currentTimeMillis() - sharedDate) >= TimeUnit.DAYS.toMillis(1)
if(isDayPassed){
viewModel.update()
}
}
}
my splash screen view model
#HiltViewModel
class SplashScreenViewModel #Inject constructor(
private val insertExchangeUseCase: InsertExchangeUseCase,
private val updateExchangeUseCase: UpdateExchangeUseCase,
private val getAllExchangeUseCase: GetAllExchangeUseCase
) : ViewModel() {
init {
insertExchanges()
}
private fun insertExchanges() {
viewModelScope.launch {
insertExchangeUseCase.insert(DEFAULT_CURRENCY)
}
}
fun update(){
viewModelScope.launch {
updateExchangeUseCase.updateExchange(getAllExchangeUseCase.get(DEFAULT_CURRENCY))
}
}
}
In view model, insert is started automatically in init.
You can’t do something like this properly inside a Composable. Composables are only for creating UI. Remember the rule about no side effects. A function like your checkDate() should be called directly in onCreate() so it isn't called over and over as your Composition recomposes.
The problem with your current checkDate function: You save the "currentDate" every single this time this function is called, and you do it before checking what value was saved there before, so it will perpetually overwrite the old value before you can use it. You're also overcomplicating things by transforming the Long time into a Date and back to a Long for no reason.
Think about the logic of what you're doing:
Update the data if it has been a day since the last time you updated it.
So to implement this, we first check if the previously saved date is more than a day old. Only if it is old do we write the new date and update the data.
Here's a basic implementation. This is assuming you want to update when it has been at least 24 hours since the last update, since that seems like what you were trying to do.
fun checkDate(context: Context, viewModel: SplashScreenViewModel) {
val sharedPreferences = context.getSharedPreferences("date", Context.MODE_PRIVATE)
val lastTimeUpdatedKey = "lastTimeUpdated"
val now = System.currentTimeMillis()
val lastTimeUpdated = sharedPreferences.getLong(lastTimeUpdatedKey, 0)
val isDayPassed = now - lastTimeUpdated > TimeUnit.DAYS.toMillis(1)
if (isDayPassed) {
sharedPreferences.edit { // this is the shortcut way to edit and apply all in one
putLong(lastTimeUpdatedKey, today)
}
viewModel.update()
}
}
I would also change "date" to something longer and more unique so you don't accidentally use it somewhere else for different shared preferences. And I would rename your update() function to something more descriptive.
Don't forget to move the function call out of your Composable!
I am trying to implement Navigation using single activity and
multiple Composable Screens.
This is my NavHost:
#Composable
#ExperimentalFoundationApi
fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = HOME.route,
viewModelProvider: ViewModelProvider,
speech: SpeechHelper
) = NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(route = HOME.route) {
with(viewModelProvider[HomeViewModel::class.java]) {
HomeScreen(
speech = speech,
viewModel = this,
modifier = Modifier.onKeyEvent { handleKeyEvent(it, this) }
) {
navController.navigateTo(it)
}
}
}
composable(route = Destination.VOLUME_SETTINGS.route) {
VolumeSettingsScreen(
viewModelProvider[VolumeSettingsViewModel::class.java]
) { navController.navigateUp() }
}
}
fun NavHostController.navigateTo(
navigateRoute: String,
willGoBackTo: String = HOME.route
): Unit = navigate(navigateRoute) {
popUpTo(willGoBackTo) { inclusive = true }
}
My screen looks like this:
#Composable
fun HomeScreen(
speech: SpeechHelper,
viewModel: HomeViewModel,
modifier: Modifier,
onNavigationRequested: (String) -> Unit
) {
MyBlindAssistantTheme {
val requester = remember { FocusRequester() }
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak(
R.string.welcome_
.withStrResPlaceholder(R.string.text_home_screen)
.toSpeechUiModel()
)
)
uiState?.let {
when (it) {
is UiState.Speak -> speech.speak(it.speechUiModel)
is UiState.SpeakRes -> speech.speak(it.speechResUiModel.speechUiModel())
is UiState.Navigate -> onNavigationRequested(it.route)
}
}
Column(
modifier
.focusRequester(requester)
.focusable(true)
.fillMaxSize()
) {
val rowModifier = Modifier.weight(1f)
Row(rowModifier) {...}
}
LaunchedEffect(Unit) {
requester.requestFocus()
}
}
}
This is the ViewModel:
class HomeViewModel : ViewModel() {
private val mutableUiState: MutableStateFlow<UiState?> = MutableStateFlow(null)
val uiState = mutableUiState.asStateFlow()
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(Destination.VOLUME_SETTINGS.route.toNavigationState())
}
}
When a button is clicked the ViewModel is called and the NavigateUiState is emitted... but it keeps being emitted after the next screen is loaded and that causes infinite screen reloading. What should be done to avoid this?
I re-implemented your posted code with 2 screens, HomeScreen and SettingScreen and stripped out some part of the UiState class and its usages.
The issue is in your HomeScreen composable, not in the StateFlow emission.
You have this mutableState
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak
)
that is being observed in one of your when block that executes a navigation callback.
uiState?.let {
when (it) {
is UiState.Navigate -> {
onNavigationRequested(it.route)
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
When your ViewModel function is called
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(UiState.Navigate(Destination.SETTINGS_SCREEN.route))
}
it will update uiState, setting its value to Navigate, observed by HomeScreen, satisfies the when block and then triggers the callback to navigate to the next screen.
Now based on the official Docs,
You should only call navigate() as part of a callback and not as part
of your composable itself, to avoid calling navigate() on every
recomposition.
but in your case, the navigation is triggered by an observed mutableState, and the mutableState is part of your HomeScreen composable.
It seems like when the navController performs a navigation and the NavHost being a Composable
#Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) { ... }
it will execute a re-composition, because of it, it will call again the HomeScreen (HomeScreen is not re-composed, its state remains the same) and because the HomeScreen's UiState value is still set to Navigate, it satisfies the when block, triggers again the callback to navigate, and NavHost re-composes, an infinite cycle is then created.
What I did (and its very ugly) is I created a boolean flag inside the viewModel, used it to wrap the callback conditionally,
uiState?.let {
when (it) {
is UiState.Navigate -> {
if (!viewModel.navigated) {
onNavigationRequested(it.route)
viewModel.navigated = true
} else {
// dirty empty else
}
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
}
and setting it to true afterwards, preventing the cycle.
I can hardly guess your compose implementation structure but I usually don't mix my one-time event actions and UiState, instead I have a separate UiEvent sealed class that will group "one-time" events such as the following:
Snackbar
Toast
Navigation
and having them emitted as a SharedFlow emissions because these events doesn't need any initial state or initial value.
Continuing further, I created this class
sealed class UiEvent {
data class Navigate(val route: String) : UiEvent()
}
use it in the ViewModel as a type (Navigate in this case),
private val _event : MutableSharedFlow<UiEvent> = MutableSharedFlow()
val event = _event.asSharedFlow()
fun onNavigateButtonClicked(){
viewModelScope.launch {
_event.emit(UiEvent.Navigate(Destination.SETTINGS_SCREEN.route))
}
}
and observe it in HomeScreen this way via LaunchedEffect, triggering the navigation in it without the callback being bound to any observed state.
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
onNavigationRequested(it.route)
}
}
}
}
This approach doesn't introduce the infinite navigation cycle and the dirty boolean checking is not needed anymore.
Also have a look this S.O post, similar to your case
I made a state with StateFlow with 2 lists. This is working good. I want to sort these lists according to a parameter that user will decide how to sort.
This is my code in ViewModel:
#HiltViewModel
class SubscriptionsViewModel #Inject constructor(
subscriptionsRepository: SubscriptionsRepository
) : ViewModel() {
private val _sortState = MutableStateFlow(
SortSubsType.ByDate
)
val sortState: StateFlow<SortSubsType> = _sortState.asStateFlow()
val uiState: StateFlow<SubscriptionsUiState> = combine(
subscriptionsRepository.getActiveSubscriptionsStream(_sortState.value),
subscriptionsRepository.getArchivedSubscriptionsStream(_sortState.value)
) { activeSubscriptions, archiveSubscriptions ->
SubscriptionsUiState.Subscriptions(
activeSubscriptions = activeSubscriptions,
archiveSubscriptions = archiveSubscriptions,
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SubscriptionsUiState.Loading
)
fun sortSubscriptions(sortType: SortSubsType) {
_sortState.value = sortType
}
}
sealed interface SubscriptionsUiState {
object Loading : SubscriptionsUiState
data class Subscriptions(
val activeSubscriptions: List<Subscription>,
val archiveSubscriptions: List<Subscription>,
) : SubscriptionsUiState
object Empty : SubscriptionsUiState
}
sortSubscriptions - is the function called from #Composable screen. Like this:
fun sortSubscriptions() {
viewModel.sortSubscriptions(sortType = selectedSortType.asSortSubsType())
isSortDialogVisible = false
}
Without the sort function, everything works. My question is how to fix this code so that the state changes when the sortState is changed. This is my first try working with StateFlow.
The problem is that when you create your uiState flow with combine, you just use the current value of sortState and never react to its changes.
You need something like this:
val uiState = sortState.flatMapLatest { sortValue ->
combine(
getActiveSubscriptionsStream(sortValue),
getArchivedSubscriptionsStream(sortValue)
) { ... }
}.stateIn(...)
So i have a few questions about using hiltviewmodels, states and remember in compose.
For some context, i have a ViewPager set up
HorizontalPager(
count = 4,
modifier = Modifier.fillMaxSize(),
state = pagerState,
) { page ->
when (page) {
0 -> PagerOne()
1 -> PagerTwo()
2 -> PagerThree()
3 -> PagerFour()
}
}
Lets say i have a State in my viewmodel declared like this
private val _data: MutableState<DataClass> = mutableStateOf(DataClass())
var data: State<DataClass> = _data
First, where do i inject my viewmodel? Is it fine to do it in the constructor of my pager composable?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
...
And if i want to get the value from that viewmodel state, do i need to wrap it into a remember lambda?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
val myState = viewmodel.data or var myState by remember { viewmodel.data }
Next question about flow and .collectasstate. Lets say i have a function in my viewmodel which returns a flow of data from Room Database.
fun getRoomdata() = roomRepository.getLatesData()
Is it correct to get the data like this in my composable?
val roomData = viewmodel.getRoomdata().collectasState(initial = emptyRoomdata())
Everything is working like expected, but im not sure these are the best approaches.
How do i ignore the viewModel's last state on the first composition of a composable?
The usecase is the following:
User enters MyDumbComposable
Clicks to add a song to a playlist
When the action is successful, popBack
This is my viewModel
#HiltViewModel
class AddToPlaylistViewModel #Inject constructor(
private val addToPlaylistUseCase: AddToPlaylistUseCase,
) : ViewModel() {
private val _state = mutableStateOf<AddToPlaylistState>(AddToPlaylistState.Initial)
val state: State<AddToPlaylistState> = _state
operator fun invoke(
params: AddToPlaylistParams
) {
addToPlaylistUseCase(params)
.flowOn(Dispatchers.IO)
.onEach { _state.value = it }
.launchIn(viewModelScope)
}
}
This is MyDumbComposable
fun MyDumbComposable(
addToPlaylistViewModel: AddToPlaylistViewModel = hiltViewModel(),
song: Song,
popBack: () -> Unit
) {
if (addToPlaylistViewModel.state.value is AddToPlaylistState.Loaded) {
LaunchedEffect(Unit) {
popBack()
}
}
fun onClick(playlist: PlaylistWithSongs) {
addToPlaylistViewModel(
AddToPlaylistParams(
selected = Selected(listOf(song)),
playlist = playlist.playlist
)
)
}
///...
It works the first time and pops correctly.
However, whenever the user returns to that
composable, the AddToPlaylistViewModel is cached
so the last value is still AddToPlaylistState.Loaded,
meaning it'll pop the screen right away.
I ended up resetting my state using a DisposableEffect:
DisposableEffect(Unit) {
onDispose {
addToPlaylistViewModel.resetState()
}
}