Button onClick keep replacing values in mutableStateList - android

I'm trying to display a 4x4 grid with values that change depending on user input. To achieve that, I created mutableStateListOf that I use in a ViewModel to survive configuration changes. However, when I try to replace a value in that particular list using button onClick, it keeps doing that until app crashes. I can't understand why is onReplaceGridContent looping after clicking the button once. Currently, my code looks like this:
ViewModel:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
fun initialize(){
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Button:
Button(
onClick = {
onReplaceGridContent(GameUtils.getRandomTileNumber(),GameUtils.getRandomTilePosition(gameGridContent))},
colors = Color.DarkGray
){
Text(text = "Add number to tile")
}
Activity Composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
gameViewModel.initialize()
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}

Your initialize will actually run on every recomposition of gameScreen:
You click on a tile - state changes causing recomposition.
initializa is called and changes the state again causing recomposition.
Step 2 happens again and again.
You should initialize your view model in its constructor instead (or use boolean flag to force one tim initialization) to make it inly once.
Simply change it to constructor:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
init {
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Now you don't need to call initialize in the composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}

Related

How can I save data to room just first time in kotlin?

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!

How to get updated results in StateFlow depending on parameter (sorting a list) with Jetpack Compose

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(...)

mutableStateListOf change not reflecting in UI - Jetpack Compose

in my ViewModel:
private val _itemList = mutableStateListOf<Post>()
val itemList: List<Post> = _itemList
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy(isLiked = true)
}
Here my Post data class:
data class Post(
val id: Int,
val name: String,
val isLiked: Boolean = false,
)
And here my Composable:
val postList = viewModel.itemList
LazyRow(content = {
items(postList.size) { i ->
val postItem = postList[i]
PostItem(
name = postItem.name,
isLiked = postItem.isLiked,
likePost = { viewModel.likePost(postItem)}
)
}
})
The change does not update in the UI instantly, I first have to scroll the updated item out of the screen so it recomposes or switch to another Screen and go back to see the change.
For some reason it doesn't like updating, it will add and delete and update instantly. You have to do it this way when updating for our to update the state.
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy()
_itemList[index].isLiked = true
}
You are returning a List<> effectively and not MutableStateList from your ViewModel.
If you want the list to not be mutable from the view, I happen to use MutableStateFlow<List<>> and return StateFlow<List<>>. You could also just convert it to a list in your composable.
Edit:
//backing cached list, or could be data source like database
private val deviceList = mutableListOf<Device>()
private val _deviceListState = MutableStateFlow<List<Device>>(emptyList())
val deviceListState: StateFlow<List<BluetoothDevice>> = _deviceListState
//manipulate and publish
fun doSomething() {
_deviceListState.value = deviceList.filter ...
}
In your UI
val deviceListState = viewModel.deviceListState.collectAsState().value

When using List as State, how to update UI when item`attribute change in Jetpack Compose?

For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())

Why this function is called multiple times in Jetpack Compose?

I'm currently trying out Android Compose. I have a Text that shows price of a crypto coin. If a price goes up the color of a text should be green, but if a price goes down it should be red. The function is called when a user clicks a button. The problem is that the function showPrice() is called multiple times (sometimes just once, sometimes 2-4 times). And because of that the user can see the wrong color. What can I do to ensure that it's only called once?
MainActivity:
#Composable
fun MainScreen() {
val priceLiveData by viewModel.trackLiveData.observeAsState()
val price = priceLiveData ?: return
when (price) {
is ViewState.Response -> showPrice(price = price.data)
is ViewState.Error -> showError(price.text)
}
Button(onClick = {viewModel.post()} )
}
#Composable
private fun showPrice(price: Double) {
lastPrice = sharedPref.getFloat("eth", 0f).toDouble()
val color by animateColorAsState(if (price >= (lastPrice)) Color.Green else
Color.Red)
Log.v("TAG", "last=$lastPrice new = $price")
editor.putFloat("eth", price.toFloat()).apply()
Text(
text = price.toString(),
color = color,
fontSize = 28.sp,
fontFamily = fontFamily,
fontWeight = FontWeight.Bold
)
}
ViewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: Repository
): ViewModel() {
private val _trackLiveData: MutableLiveData<ViewState<Double>> = MutableLiveData()
val trackLiveData: LiveData<ViewState<Double>>
get() = _trackLiveData
fun post(
) = viewModelScope.launch(Dispatchers.Default) {
try {
val response = repository.post()
_trackLiveData.postValue(ViewState.Response(response.rate.round(7)))
} catch (e: Exception) {
_trackLiveData.postValue(ViewState.Error())
Log.v("TAG: viewmodelPost", e.message.toString())
}
}
}
ViewState:
sealed class ViewState<out T : Any> {
class Response<out T : Any>(val data: T): ViewState<T>()
class Error(val text:String = "Unknown error"): ViewState<Nothing>()
}
So when I press Button to call showPrice(). I can see these lines on Log:
2021-06-10 16:39:18.407 16781-16781/com.myapp.myapp V/TAG: last=2532.375732421875 new = 2532.7403716
2021-06-10 16:39:18.438 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
2021-06-10 16:39:18.520 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
What can I do to ensure that it's only called once?
Nothing, that's how it's meant to work. In the View system you would not ask "Why is my view invalidated 3 times?". The framework invalidates (recomposes) the view as it needs, you should not need to know or care when that happens.
The issue with your code is that your Composable is reading the old value from preferences, that is not how it should work, that value should be provided by the viewmodel as part of the state. Instead of providing just the new price, expose a Data Class that has both the new and old price and then use those 2 values in your composable to determine what color to show, or expose the price and the color to use.

Categories

Resources