I have an app that reads properties from a file. The file can be changed outside of my application, it can be deleted or fully replaced. My goal is to read the .properties
file and show a preferences like screen where they can modify each property.
This is my first app with compose and I am not sure how to achieve this.
From what I have read so far in order for the state to be reflected in your UI you need to change the entire instance of array/properties. From my understanding that is going to trigger a recomposition of all elements. That be be avoided with SnapshotStateMap
(?).
So my code currently looks like this:
View model
@HiltViewModel(assistedFactory = SettingsViewModel.Factory::class)
class SettingsViewModel @AssistedInject constructor(@Assisted properties: Properties) : ViewModel() {
private val prefs = SnapshotStateMap<String, Any>()
init {
properties.forEach {
prefs[it.key.toString()] = it.value
}
}
fun getPref(key: String, default: String): String {
return prefs[key]?.toString() ?: default
}
fun updatePref(key: String, value: Any) {
prefs[key] = value
}
...
}
// Pref class
data class Pref<T>(val key: String, @StringRes val title: Int, val default: T)
UI
@Composable
fun SettingsScreen() {
val context = LocalContext.current
val vm: SettingsViewModel = hiltViewModel<SettingsViewModel, SettingsViewModel.Factory>(
creationCallback = { factory ->
factory.create(FileUtils.getProperties(context))
}
)
SettingsColumn {
SettingsSwitchComp(
pref = Prefs.pref1,
getValue = { key, default -> vm.getPref(key, default) },
onClick = { vm.updatePref(Prefs.pref1.key, !it) }
)
SettingsSwitchComp(
pref = Prefs.pref2,
getValue = { key, default -> vm.getPref(key, default) },
onClick = { vm.updatePref(Prefs.pref2.key, !it) }
)
}
}
@Composable
fun SettingsSwitchComp(
pref: Pref<Boolean>,
getValue: (String, Boolean) -> Boolean,
onClick: (Boolean) -> Unit
) {
val value = getValue(pref.key, pref.default)
Surface(
color = Color.Transparent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onClick = { onClick(value) },
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(id = pref.title),
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Start,
)
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = getValue(pref.key, pref.default),
onCheckedChange = { onClick(value) }
)
}
HorizontalDivider()
}
}
}
@Composable
fun SettingsColumn(content: @Composable () -> Unit) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
content()
}
}
But if I toggle one switch, both of them get recomposed. So I am obviously missing something about the usage of SnapshotStateMap
…
Any advice appreciated.
2
The problem was in the way a was sending the value to my composables:
getValue = { key, default -> vm.getPref(key, default) }
sending it as a function triggered unnecessary recompositions. Instead when using:
value = vm.getPref(key, default)
only the composable that has been updated has triggered a recomposition.