Expose value from SharedPreferences as Flow - kotlin

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)
}

Related

Stripe Payment API integration to Jetpack Compose

I can't figure it out, how to impalement Stripe API integration into an Compose app
Here is Stripe provided snippet of code
class CheckoutActivity : AppCompatActivity() {
lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paymentSheet = PaymentSheet(this, ::onPaymentSheetResult)
}
fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
// implemented in the next steps
}
}
In my case I am out of ideas where to put paymentSheet = PaymentSheet(this, ::onPaymentSheetResult) in compose code as it shows that:
None of the following functions can be called with the arguments supplied.
(ComponentActivity, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet
(Fragment, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet
class MainActivity : ComponentActivity() {
lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PayTheme {
LoginUi()
}
}
fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
// implemented in the next steps
}
}
}
First of all, you can check the stripe compose sample on github stripe-android (ComposeExampleActivity.kt).
Add stripe dependency
implementation "com.stripe:stripe-android:20.17.0"
Initialize stripe PaymentConfiguration in the Application class
#HiltAndroidApp
class BookstoreApplication : Application() {
override fun onCreate() {
super.onCreate()
PaymentConfiguration.init(applicationContext, BuildConfig.STRIPE_PUBLISHABLE_KEY)
}
}
Stripe provides many ways to implement payments in the app. Let's consider payment confirmation using PaymentSheetContract and PaymentLauncher.
Example #1: Confirm payment using PaymentSheetContract
In this case, we should use rememberLauncherForActivityResult() with PaymentSheetContract() to launch stripe payment form.
PaymentScreen.kt (Compose)
#Composable
fun PaymentScreen(
viewModel: PaymentViewModel = hiltViewModel()
) {
val stripeLauncher = rememberLauncherForActivityResult(
contract = PaymentSheetContract(),
onResult = {
viewModel.handlePaymentResult(it)
}
)
val clientSecret by viewModel.clientSecret.collectAsStateWithLifecycle()
clientSecret?.let {
val args = PaymentSheetContract.Args.createPaymentIntentArgs(it)
stripeLauncher.launch(args)
viewModel.onPaymentLaunched()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = {
viewModel.makePayment()
}
) {
Text(text = "Confirm payment")
}
}
}
PaymentViewModel.kt
#HiltViewModel
class PaymentViewModel #Inject constructor(
private val repository: PaymentRepository
) : ViewModel() {
private val _clientSecret = MutableStateFlow<String?>(null)
val clientSecret = _clientSecret.asStateFlow()
fun makePayment() {
val paymentIntent = repository.createPaymentIntent()
_clientSecret.update { paymentIntent.clientSecret }
}
fun onPaymentLaunched() {
_clientSecret.update { null }
}
fun handlePaymentResult(result: PaymentSheetResult) {
when(result) {
PaymentSheetResult.Canceled -> TODO()
PaymentSheetResult.Completed -> TODO()
is PaymentSheetResult.Failed -> TODO()
}
}
}
Example #2: Confirm payment using PaymentLauncher
In this case, we should use rememberLauncherForActivityResult() with PaymentSheetContract() to launch stripe payment form.
PaymentScreen.kt (Compose)
#Composable
fun PaymentScreen(
viewModel: PaymentViewModel = hiltViewModel()
) {
val paymentLauncher = rememberPaymentLauncher(viewModel::handlePaymentResult)
val confirmPaymentParams by viewModel.confirmPaymentParams.collectAsStateWithLifecycle()
confirmPaymentParams?.let { payment ->
paymentLauncher.confirm(payment)
viewModel.onPaymentLaunched()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = {
viewModel.makePayment()
}
) {
Text(text = "Confirm payment")
}
}
}
#Composable
fun rememberPaymentLauncher(
callback: PaymentLauncher.PaymentResultCallback
): PaymentLauncher {
val config = PaymentConfiguration.getInstance(LocalContext.current)
return PaymentLauncher.rememberLauncher(
publishableKey = config.publishableKey,
stripeAccountId = config.stripeAccountId,
callback = callback
)
}
PaymentViewModel.kt
#HiltViewModel
class PaymentViewModel #Inject constructor(
private val repository: PaymentRepository
) : ViewModel() {
private val _confirmPaymentParams = MutableStateFlow<ConfirmPaymentIntentParams?>(null)
val confirmPaymentParams = _confirmPaymentParams.asStateFlow()
fun makePayment() {
val paymentIntent = repository.createPaymentIntent()
// For example, pay with hardcoded test card
val configuration = ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams(
paymentMethodCreateParams = PaymentMethodCreateParams.create(
card = PaymentMethodCreateParams.Card(
number = "4242424242424242",
expiryMonth = 1,
expiryYear = 24,
cvc = "111"
)
),
clientSecret = paymentIntent.clientSecret
)
_confirmPaymentParams.update { configuration }
}
fun onPaymentLaunched() {
_confirmPaymentParams.update { null }
}
fun handlePaymentResult(result: PaymentResult) {
when(result) {
PaymentResult.Canceled -> TODO()
PaymentResult.Completed -> TODO()
is PaymentResult.Failed -> TODO()
}
}
}
Data layer
The functions described below should be implemented somewhere on the server side. So, the client should only request some data from payment intent (client_secret for example).
Please, read stripe Accept a payment doc to understand better.
You can also watch youtube video: How to integrate Stripe in Android Studio 2022.
PaymentRepository.kt
class PaymentRepository #Inject constructor(
private val stripeApiService: StripeApiService,
private val paymentDao: PaymentDao
) {
/*
Create customer before payment (attach to app user)
*/
suspend fun createCustomer() = withContext(Dispatchers.IO) {
val customer = stripeApiService.createCustomer()
// save customer in the database or preferences
// customerId required to confirm payment
paymentDao.insertCustomer(customer)
}
suspend fun refreshCustomerEphemeralKey() = withContext(Dispatchers.IO) {
val customer = paymentDao.getCustomer()
val key = stripeApiService.createEphemeralKey(customer.customerId)
paymentDao.insertEphemeralKey(key)
}
suspend fun createPaymentIntent() = withContext(Dispatchers.IO) {
val customer = paymentDao.getCustomer()
refreshCustomerEphemeralKey()
val paymentIntent = stripeApiService.createPaymentIntent(
customerId = customer.customerId,
amount = 1000,
currency = "usd", // or your currency
autoPaymentMethodsEnable = true
)
return#withContext paymentIntent
}
}
StripeApiService.kt
private const val SECRET = BuildConfig.STRIPE_SECRET_KEY
interface StripeApiService {
#Headers(
"Authorization: Bearer $SECRET",
"Stripe-Version: 2022-08-01"
)
#POST("v1/customers")
suspend fun createCustomer() : CustomerApiModel
#Headers(
"Authorization: Bearer $SECRET",
"Stripe-Version: 2022-08-01"
)
#POST("v1/ephemeral_keys")
suspend fun createEphemeralKey(
#Query("customer") customerId: String
): EphemeralKeyApiModel
#Headers(
"Authorization: Bearer $SECRET"
)
#POST("v1/payment_intents")
suspend fun createPaymentIntent(
#Query("customer") customerId: String,
#Query("amount") amount: Int,
#Query("currency") currency: String,
#Query("automatic_payment_methods[enabled]") autoPaymentMethodsEnable: Boolean,
): PaymentIntentApiModel
}

Will the operation of collect from Flow cost many system resources when I use Compose with Kotlin?

The SoundViewModel is a ViewModel class, and val listSoundRecordState may be used by some modules in the App.
In Code A, I invoke fun collectListSoundRecord() when I need to use the data listSoundRecordState. But fun collectListSoundRecord() may be launched again and again because of Jetpack Compose recomposition, I don't know if it will cost many system resources?
In Code B, I launch private fun collectListSoundRecord() in init { }, collectListSoundRecord() will be launched only one time, but it will persist in memory until the App code closed even if I needn't to use the data listSoundRecordState, will the way cost many system resources?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { }
//It may be launched again and again
fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { collectListSoundRecord() }
private fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
You would probably benefit from collecting the original flow (from listRecord()) only when there is a subscriber to your intermediate flow (the one you keep in your SoundViewModel) and cache the results.
A subscriber, in your case, would be a Composable function that collects the values (and may recompose often).
You can achieve this using the non-suspending variant of stateIn(), since you have a default value.
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
val listSoundRecordState = listRecord().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Result.Loading)
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
In order to use the StateFlow from the UI layer (a #Composable function), you will have to transform it into a State, like so:
val viewModel: SoundViewModel = viewModel()
val listSoundRecord = viewModel.listSoundRecordState.collectAsState()
I use the below implementation of composable with view-model using LaunchedEffect
PokemonDetailScreen.kt
#Composable
fun PokemonDetailScreen(
dominantColor: Color,
pokemonName: String,
navController: NavController,
topPadding: Dp = 20.dp,
pokemonImageSize: Dp = 200.dp,
viewModel: PokemonDetailVm = hiltViewModel()
) {
var pokemonDetailData by remember { mutableStateOf<PokemonDetailView>(PokemonDetailView.DisplayLoadingView) }
val pokemonDetailScope = rememberCoroutineScope()
LaunchedEffect(key1 = true){
viewModel.getPokemonDetails(pokemonName)
viewModel.state.collect{ pokemonDetailData = it }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {
PokemonHeader(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.2f)
.align(Alignment.TopCenter)
) {
navController.popBackStack()
}
PokemonBody(
pokemonInfo = pokemonDetailData,
topPadding = topPadding,
pokemonImageSize = pokemonImageSize
) {
pokemonDetailScope.launch {
viewModel.getPokemonDetails(pokemonName)
}
}
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
) {
if(pokemonDetailData is PokemonDetailView.DisplayPokemonView){
val data = (pokemonDetailData as PokemonDetailView.DisplayPokemonView).data
data.sprites.let {
// Image is available
val url = PokemonUtils.formatPokemonDetailUrl(it.frontDefault)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = data.name,
contentScale = ContentScale.Fit,
modifier = Modifier
// Set the default size passed to the composable
.size(pokemonImageSize)
// Shift the image down from the top
.offset(y = topPadding)
)
}
}
}
}
}
PokemonDetailVm.kt
#HiltViewModel
class PokemonDetailVm #Inject constructor(
private val repository: PokemonRepositoryFeature
): ViewModel(){
private val _state = MutableSharedFlow<PokemonDetailView>()
val state = _state.asSharedFlow()
suspend fun getPokemonDetails(pokemonName:String) {
viewModelScope.launch {
when(val pokemonInfo = repository.getPokemonInfo(pokemonName)){
is Resource.Error -> {
pokemonInfo.message?.let {
_state.emit(PokemonDetailView.DisplayErrorView(message = it))
}
}
is Resource.Loading -> {
_state.emit(PokemonDetailView.DisplayLoadingView)
}
is Resource.Success -> {
pokemonInfo.data?.let {
_state.emit(PokemonDetailView.DisplayPokemonView(data = it))
}
}
}
}
}
}

ObjectBox returning all elements ignoring pageSize while using paging3

I want to get 100 items per page from ObjectBox database using Paging3 library. But I'm getting all the elements from database at once. The official document of ObjectBox have information about paging2.
Here is my Implementation:
LocalDatabaseImpl.kt
LocalDatabaseImpl(
// dependencies...
private val trxBox: Box<Trx>
) {
override fun getPagingDataSource(): ObjectBoxDataSource.Factory<Trx> {
val query = trxBox.query().build()
return ObjectBoxDataSource.Factory(query)
}
}
ViewModel.kt
#HiltViewModel
class HomeViewModel #Inject constructor(
private val db: LocalDatabase
): ViewModel() {
private val pager: Pager<Int, Trx>
get() = Pager(
config = PagingConfig(pageSize = 100),
pagingSourceFactory = db.getPagingDataSource()
.asPagingSourceFactory(Dispatchers.IO)
)
private var _trxFlow = pager.flow.cachedIn(viewModelScope)
val trxFlow: Flow<PagingData<Trx>> get() = _trxFlow
}
Inside Compose
#Composable
fun TrxContent() {
// ...
val trxItems = viewModel.trxFlow.collectAsLazyPagingItems()
// latest trx
LazyColumn () {
items(items = trxItems) { trx ->
if (trx == null) return#items
ItemTrxCompose(trx)
}
}
}

Jetpack Compose - UI not updating when flow is changed

I have the following Composable view and view model. The problem I'm having is that even though isLoading is being updated in the view model, it's not updating the view at all, it's stuck on the LoadingUi.
#Composable
fun MediaDetailsPage(
mediaId: Long?,
viewModel: DetailsViewModel
) {
LaunchedEffect(Unit, block = {
viewModel.fetchDetailsOf(mediaId!!)
})
val isLoading by viewModel.isLoading.collectAsState()
val isError by viewModel.error.collectAsState()
when {
isLoading -> LoadingUi()
isError || viewModel.model == null -> ErrorUi()
else -> MediaDetailsUi(viewModel.model!!)
}
}
class MovieDetailsViewModel(
private val useCase: MovieDetailsUseCase = config.movieDetailsUseCase
) : DetailsViewModel() {
override fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher){
if(model != null) return
fetchFrom({ useCase.getMovieDetailsOf(id) }, dispatcher)
}
}
abstract class DetailsViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow(false)
val error: StateFlow<Boolean> = _error
var model: MediaDetailsModel? = null
abstract fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher = Dispatchers.IO)
protected fun fetchFrom(
useCaseCall: suspend () -> Either<MediaDetails?, ErrorEntity?>,
dispatcher: CoroutineDispatcher
) {
job( {
_isLoading.value = true
val result = useCaseCall.invoke()
if (result.isSuccess) model = result.body!!.convert()
else _error.value = true
_isLoading.value = false
}, dispatcher)
}
}
The most confusing thing is that I'm doing pretty much identically this elsewhere in the app and that is working perfectly. I was under the impression that a composable view refreshes when a flow variable is updated, or have I got the wrong end of the stick here?

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.