how to add a points system to an app preferences DataStore Jetpack Compose - kotlin

I'm working on a Quiz app and I'm trying to add a points system to the app so that everytime the user gets a question right he gets a +1pts.
and for storing the points I use jetpack compose preferences Datastore.
the problem is whenever I want to add a point to the already saved points it doesn't work.
this is my PointsData
class PointsData(private val context: Context) {
//create the preference datastore
companion object{
private val Context.datastore : DataStore<Preferences> by preferencesDataStore("points")
val CURRENT_POINTS_KEY = intPreferencesKey("points")
}
//get the current points
val getpoints: Flow<Int> =context.datastore.data.map { preferences->
preferences[CURRENT_POINTS_KEY] ?: 0
}
// to save current points
suspend fun SaveCurrentPoints(numPoints : Int){
context.datastore.edit {preferences ->
preferences[PointsData.CURRENT_POINTS_KEY] = numPoints
}
}
}
save points methode
class SavePoints {
companion object {
#Composable
fun savepoints(numPointsToSave : Int) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val datastore = PointsData(context)
LaunchedEffect(1) {
scope.launch {
datastore.SaveCurrentPoints(numPointsToSave)
}
}
}
}
}
and whenever i want to get the number of points from my DataStore i use
val pointsdatastore = PointsData(context)
val currentpoints = pointsdatastore.getpoints.collectAsState(initial = 0)
//display it as text for example
Text(text = currentpoints.value.toString(), fontSize = 30.sp, fontWeight = FontWeight.Bold,
color = Color.White)
and to do the operation i want (add +1 o the already existing points i do this
val pointsdatastore = PointsData(context)
val currentpoints = pointsdatastore.getpoints.collectAsState(initial = 0)
SavePoints.savepoints(numPointsToSave = currentpoints.value + 1)
but it doesn't seem to work because the number of points always stays 1.
if you know whats the problem please help.

I found the answer my self but for anyone who is stuck in same situation the solution is to another method in PointsData(look at the question provided code)
the method is:
suspend fun incrementpoints(){
context.datastore.edit { preferences->
val currentpoints = preferences[CURRENT_POINTS_KEY] ?: 0
preferences[CURRENT_POINTS_KEY] = currentpoints + 1
}
}
(if you want decrement not increment you can just change + to - )
and now in the PointsMethod(look at the question provided code) you should add
#Composable
fun incrementpoints() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val datastore = PointsData(context)
LaunchedEffect(key1 = 1) {
scope.launch {
datastore.incrementpoints()
}
}
}

Related

How to add the views on the second line if there is no space on the first line?

I am trying to split some words on two lines to create a sentence. When there is no more space on the first line, the words should automatically go to the second line, but no matter what I have tried so far, only the first line is used, while the second line remains empty all the time.
Here is a screen capture.
MainActivity:
class MainActivity : AppCompatActivity(), RemoveAnswerListener {
private var binding: ActivityMainBinding? = null
var listAnswers = mutableListOf<Answer>()
private lateinit var actualAnswer: List<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
actualAnswer = listOf(
"What",
"did",
"you",
"want",
"to",
"ask",
"me",
"yesterday?"
)
val answerOption = listOf(
"me",
"yesterday?",
"did",
"to",
"want",
"you",
"ask",
"What"
)
answerOption.forEach {
val key = TextView(binding?.answerBox?.context)
with(key){
binding?.answerBox?.addView(this)
setBackgroundColor(Color.WHITE)
text = it
textSize = 18F
setPadding(40, 20, 40, 20)
val margin = key.layoutParams as FlexboxLayout.LayoutParams
margin.setMargins(30, 30, 30, 30)
layoutParams = margin
setOnClickListener {
moveToAnswer(it)
}
}
}
}
private fun moveToAnswer(view: View) {
if(listAnswers.size < actualAnswer.size){
view.setOnClickListener(null)
listAnswers.add(Answer(view, view.x, view.y, (view as TextView).text.toString(), this#MainActivity))
val topPosition = binding?.lineFirst?.y?.minus(120F)
// val topPosition1 = binding?.lineSecond?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
// var leftPosition1 = binding?.lineSecond?.x
if (listAnswers.size > 0) {
var allWidth = 0F
listAnswers.forEach {
allWidth += (it.view?.width)?.toFloat()!! + 20F
}
allWidth -= (view.width).toFloat()
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}else{
// leftPosition1 = binding?.lineSecond?.x?.plus(allWidth)
}
}
val completeMove = object: Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
}
}
if (leftPosition != null) {
if (topPosition != null) {
view.animate()
.setListener(completeMove)
.x(leftPosition)
.y(topPosition)
}
}
}
}
}
And this is the data class "Answer":
data class Answer(var view: View? = null,
var actualPositionX: Float = 0F,
var actualPositionY: Float = 0F,
var text: String = "",
var removeListener: RemoveAnswerListener? = null
){
init {
view?.setOnClickListener {
it.animate()
.x(actualPositionX)
.y(actualPositionY)
removeListener?.onRemove(this)
}
}
}
interface RemoveAnswerListener{
fun onRemove(answer: Answer)
}
You don't seem to be offsetting the View to the second line anywhere? You just initialise its position to the start of the first line:
val topPosition = binding?.lineFirst?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
And then you adjust the x position if there's room for it, otherwise it stays at the start
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}
(unless I'm misunderstanding things - I don't know what lineFirst is, a containing view for the first line I guess)
So you're not moving the view down, or to lineSecond or whatever - and you're not adjusting the x position based on the contents of the second line either.
Honestly if I were you, I'd look into the Flow helper for ConstraintLayout - it works like flexbox and basically moves elements below as they fill up the horizontal space, so it automatically does what you're doing here. Here's a guide on it that should give you the idea. Might save you a lot of hassle!

My response data return " kotlinx.coroutines.flow.SafeFlow#1493a74"

I am trying to do dictionary app using kotlin language. I built the project with mvvm and clean architecture. I have been trying to pull vocabulary information from the internet using jsoap. I am using flow for data. I couldnt find where the issiue is. Normally, the words should appear on the screen or I should be able to see the data when I println on the console.But I can't see it on the screen or on the console, probably because the data coming from the internet is as follows.
kotlinx.coroutines.flow.SafeFlow#1493a74
I am sharing my codes below
ExtractedData
data class ExtractedData(
var id :Int = 0,
var word:String = "",
var meaning :String = ""
)
I created ExtractedData class to represent vocabulary or word data from internet
WordInfoRepositoryImpl
class WordInfoRepositoryImpl #Inject constructor(
private val api:DictionaryApi
) : WordInfoRepository {
//get words with meanings on the internet using jsoap
override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
emit(Resource.Loading())
val listData = mutableListOf<ExtractedData>()
try {
val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
val doc = withContext(Dispatchers.IO){
Jsoup.connect(url).get()//-->Here it gives the following warning even though I have it in withContext `Inappropriate blocking method call`
}
val table = doc.select("table")
val rows = table.select("tr")
val eventsSize = rows.size
for (i in 1 until eventsSize) {
val row = rows[i]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
listData.add(ExtractedData(i,word,meaning))
}
}
catch (e: IOException) {
emit(Resource.Error("IO Exception"))
}
catch (e : HttpException) {
emit(Resource.Error("HTTP EXCEPTION"))
}
emit(Resource.Success(listData))
}
}
getEventsList is in my WordInfoRepositoryImpl class in my data layer here I am pulling data from internet using jsoap
WordInfoRepository
interface WordInfoRepository {
fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}
this is the interface that I reference wordInforepositoryImpl in the data layer in my interface domain layer
GetWordsAndMeaningsOnTheInternetUseCase
class GetWordsAndMeaningsOnTheInternetUseCase#Inject constructor(
private val repository: WordInfoRepository
){
operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
return repository.getEventsList()
}
}
GetWordsAndMeaningsOnTheInternetUseCase is my usecase in my domain layer
ViewModel
#HiltViewModel
class MostUsedWordScreenViewModel #Inject constructor(
private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {
private var searchJob: Job? = null
private val _state = mutableStateOf(MostUsedWordState())
val state: State<MostUsedWordState> = _state
init {
fetchData()
}
private fun fetchData() {
searchJob?.cancel()
searchJob = viewModelScope.launch(IO) {
getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}
}
}
}
MostUsedWordScreen
#Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
val state = viewModel.state.value
println("state --- >>> "+state.mostWordUsedItems)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.mostWordUsedItems.size) { i ->
val wordInfo = state.mostWordUsedItems[i]
if(i > 0) {
Spacer(modifier = Modifier.height(8.dp))
}
MostUsedWordItem(word = wordInfo)
if(i < state.mostWordUsedItems.size - 1) {
Divider()
}
}
}
}
#Composable
fun MostUsedWordItem(word : ExtractedData ) {
// println("this is MostUsedWordItem")
Column(modifier = Modifier
.padding(5.dp)
.fillMaxWidth()) {
Text(text = word.word,
modifier = Modifier.padding(3.dp),
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
It is included in the MostUsedWordScreenViewModel and MostUsedWordScreen presententation layer
Where I println("state --- >>> "+state.mostWordUsedItems) in MostUsedWordScreen, the state console shows as empty like this System.out: state --- >>> []
I tried to explain as detailed as I can, I hope you can understand.
A Flow doesn't do anything until you call a terminal operator on it. You called onEach, which is not a terminal operator. You should use collect. Or you can avoid the nesting inside a launch block by using onEach and launchIn, which does the same thing as launching a coroutine and calling collect() on the flow. You don't need to specify Dispatchers.IO here because nothing in your Flow is blocking. You correctly wrapped the blocking call in withContext(Dispatchers.IO), and the warning is a false positive. That's a well-known bug in their compiler inspection.
searchJob = getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}.launchIn(viewModelScope)
By the way, you need to move your emit(Success...) inside your try block. The way it is now, when there is an error, the error will immediately get replaced by a Success with empty list.
Side note, I recommend avoiding passing MutableLists around between classes. You have no need for them and it's a code smell. Sharing mutable state between classes is error-prone. I don't think there is any justification for using a Flow<MutableList> instead of a Flow<List>.
You rarely even need a MutableList locally in a function. For example, you could have done in your try block:
val listData = List(eventsSize - 1) {
val row = rows[it + 1]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
ExtractedData(i,word,meaning)
}
emit(Resource.Success(listData))

Expose value from SharedPreferences as Flow

I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}

Seting a Query in Firestore Cloud

I have a question becouse i wrote this with a guide and it displays me all of the documents and i wanna that it displays only this with one i login in to my app and its will be nice when this wil be in 1 line and no i 2 seperates but it is not so necessary
val imie="Sebastian"
val query = db.collection("users").whereEqualTo("welcome",imie).orderBy("displayName")
val options = FirestoreRecyclerOptions.Builder<User>().setQuery(query, User::class.java)
.setLifecycleOwner(this).build()
val adapter = object: FirestoreRecyclerAdapter<User, UserViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(this#MainActivity).inflate(android.R.layout.simple_list_item_2,parent,false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int, model: User) {
val tvWelcome: TextView=holder.itemView.findViewById(android.R.id.text1)
val tvName: TextView=holder.itemView.findViewById(android.R.id.text2)
tvWelcome.text = model.welcome
tvWelcome.text = model.displayName
tvName.text = model.displayName
}
}
val uzytkownik: RecyclerView = findViewById(R.id.uzytkownik)
uzytkownik.adapter = adapter
uzytkownik.layoutManager = LinearLayoutManager(this)
Firebase console
val query = db.collection("users").whereEquelTo("firebasefieldName",youvalue)
Youvalue will write the data of the person you want to see.
eg:"Sebastian"
firebasefieldName will be your domain name in firebase
eg:displayName
Additional Information:
db.collection("users").whereEquelTo("fieldName1",youvalue).orderBy("fieldName2")
lists the listing in descending order of equal value
but after doing that you will get an error message there will be a link to that error message go to it and confirm everything.
Please support if it worked.
In this way, the data is updated instantly, for example, add a data to the table and it will appear in the list directly.
You can adjust it yourself.
Looks like I can't help any other way either. Will you still vote for me?
fun realtimeList(
collectionPath: String,
context: Context,
cevapField: String,
cevapdurum: Boolean,
field: String,
equel: String,
query: Query.Direction
): LiveData<MutableList<Any>> {
val mutableData = MutableLiveData<MutableList<Any>>()
val docRef = Firebase.firestore.collection(collectionPath)
docRef
.whereEqualTo(field, equel)
.whereEqualTo(cevapField, cevapdurum)
.orderBy("date", query)
.addSnapshotListener { querySnapshot, firebaseFirestoreException ->
firebaseFirestoreException?.let {
context.showShortToast(it.message.toString())
return#addSnapshotListener
}
val listData: MutableList<Any> = mutableListOf()
querySnapshot?.let {
for (document in it) {
val polyclinic = document.getString("policinic")
val title = document.getString("title")
val description = document.getString("descripiton")
val date: Long = document.get("date") as Long
val userid = document.getString("userid")
val quetinid = document.id
val cevapDurum = document.get("cevapdurum")
val questionio =
Questionio(
polyclinic!!,
title!!,
description!!,
SimpleDateFormat(date, "dd MMMM yyyy"),
SimpleDateFormat(date, "HH:mm"),
userid!!,
quetinid,
cevapDurum as Boolean
)
listData.add(questionio)
}
mutableData.value = listData
}
}
return mutableData
}
Part of my list adapter class
inner class ListViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bindView(questionio: Questionio) {
itemView.findViewById<TextView>(R.id.txt_policlinic).text = questionio.policlinic
itemView.findViewById<TextView>(R.id.txt_title).text = questionio.title
itemView.findViewById<TextView>(R.id.txt_description).text = questionio.description
itemView.findViewById<TextView>(R.id.txt_date).text = questionio.date
itemView.findViewById<TextView>(R.id.txt_time).text = questionio.time
val txtCevapDurum = itemView.findViewById<TextView>(R.id.txt_CevapDurum)
if (questionio.cevapdurum == true) {
txtCevapDurum.setText(R.string.answered)
txtCevapDurum.setTextColor(Color.GREEN)
} else {
txtCevapDurum.setText(R.string.not_answered)
txtCevapDurum.setTextColor(Color.RED)
}
}
init {
itemView.setOnClickListener {
listener.onItemClick(adapterPosition)
}
}
}
videos
https://www.youtube.com/watch?v=TIub9JMYhDs&t=226s&ab_channel=PhilippLackner
https://www.youtube.com/watch?v=Lfum5cO8LHA&ab_channel=SociedadAndroide

Is a good way to use State<Boolean> in view model with Android Compose?

I have read some sample codes for learning Compose.
I find many sample projects use Code A to create a StateFlow in view model, then convert it to State in #Composable function, the UI will be updated automatically when drawerOpen is changed.
1: I think both Code B and Code C can do the same thing, right? Why does many projects seldom to use them?
2: Is Code A a good way ?
3: I needn't to add rememberSaveable for variable drawerOpen in #Composable fun myRoute(...) because view model will store data, right?
Code A
class MainViewModel : ViewModel() {
private val _drawerShouldBeOpened = MutableStateFlow(false)
val drawerShouldBeOpened: StateFlow<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen by MainViewModel.drawerShouldBeOpened.collectAsState() //Do I need add rememberSaveable ?
...
}
Code B
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = mutableStateOf(false)
val drawerShouldBeOpened: State<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = MainViewModel.drawerShouldBeOpened //Do I need add rememberSaveable ?
...
}
Code C
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = false
val drawerShouldBeOpened: Boolean = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = rememberSaveable { mutableStateOf(MainViewModel.drawerShouldBeOpened)) //Can I remove rememberSaveable ?
}
There are multiple questions here.
Let me answer whatever is possible.
1. Where should you use remember / rememberSaveable? (Code A, B, or C)
Only in code C it is required.
(No issues in using in code A and B as well, but no advantages there)
Reason,
In code A and B - the state is maintained in the view model. Hence the value survives recomposition.
But in code C, the state is created and maintained inside the composable. Hence remember is required for the value to survive recomposition.
More details in Docs
2. Why Code C is not used much?
Composable recomposition happens whenever there is a change in state, not the value.
Given below is a simple example to demonstrate the same.
class ToggleViewModel : ViewModel() {
private val _enabledStateFlow = MutableStateFlow(false)
val enabledStateFlow: StateFlow<Boolean> = _enabledStateFlow
private val _enabledState = mutableStateOf(false)
val enabledState: State<Boolean> = _enabledState
private var _enabled = false
val enabled: Boolean = _enabled
fun setEnabledStateFlow(isEnabled: Boolean) {
_enabledStateFlow.value = isEnabled
}
fun setEnabledState(isEnabled: Boolean) {
_enabledState.value = isEnabled
}
fun setEnabled(isEnabled: Boolean) {
_enabled = isEnabled
}
}
#Composable
fun BooleanToggle(
viewmodel: ToggleViewModel = ToggleViewModel(),
) {
val enabledStateFlow by viewmodel.enabledStateFlow.collectAsState()
val enabledState by viewmodel.enabledState
val enabled by rememberSaveable {
mutableStateOf(viewmodel.enabled)
}
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledStateFlow) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledStateFlow(!enabledStateFlow) }) {
Text("Toggle State Flow")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledState) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledState(!enabledState) }) {
Text("Toggle State")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabled) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabled(!enabled) }) {
Text("Toggle Value")
}
}
}
}
You can see that the third text will NOT update on clicking the button.
The reason is that the mutable state inside the composable was created using an initial value from the view model data. But further updates to that data will not be reflected in the composable.
To get updates, we have to use reactive data like Flow, LiveData, State, and their variants.
3. Using StateFlow vs State.
From the docs, you can see that compose supports Flow, LiveData and RxJava.
You can see in the usage that we are using collectAsState() for StateFlow.
The method converts StateFlow to State. So both can be used.
Use Flow if the layers beyond ViewModel (like repo) are the data sources and they use Flow data type.
Else MutableState should be fine.