I am struggling to understand what is the best way to get this to work.
I have some input fields and I created a TextFieldState to keep all the state in one place.
But it is not triggering a re-composition of the composable so the state never updates.
I saw this stack overflow answer on a similar question, but I just find it confusing and it doesn't make sense to me
Here is the code:
The Composable:
#Composable
fun AddTrip (
addTripVm: AddTripVm = hiltViewModel()
) {
var name = addTripVm.getNameState()
var stateTest = addTripVm.getStateTest()
Column(
//verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Text(text = "Add Trip")
Column(
){
println("From Composable: ${name.value.value}") //No Recomposition
meTextField(
value = name.value.value,
onChange = {
addTripVm.updateName(it)
},
placeholder = "Name",
)
}
View Model code:
#HiltViewModel
class AddTripVm #Inject constructor(
private val tripRepository: TripRepositoryContract,
private val tripValidator: TripValidatorContract
): TripValidatorContract by tripValidator, ViewModel() {
/**
* Name of the trip, this is required
*/
private val nameState: MutableState<TextFieldState> = mutableStateOf(TextFieldState())
private var stateTest = mutableStateOf("");
fun updateStateTest(newValue: String) {
stateTest.value = newValue
}
fun getStateTest(): MutableState<String> {
return stateTest
}
fun getNameState(): MutableState<TextFieldState> {
return nameState;
}
fun updateName(name: String) {
println("From ViewModel? $name")
nameState.value.value = name
println("From ViewModel after update: ${nameState.value.value}") //Updates perfectly
}
}
Text field state:
data class TextFieldState(
var value: String = "",
var isValid: Boolean? = null,
var errorMessage: String? = null
)
Is this possible? Or do I need to separate the value as a string and keep the state separate for if its valid or not?
You don't change instance of nameState's value with
nameState.value.value = name
It's the same object which State checks by default with
fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> =
StructuralEqualityPolicy as SnapshotMutationPolicy<T>
private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> {
override fun equivalent(a: Any?, b: Any?) = a == b
override fun toString() = "StructuralEqualityPolicy"
}
MutableState use this as
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
Easiest way is to set
nameState.value = nameState.value.copy(value= name)
other option is to write your own SnapshotMutationPolicy
Related
Am new to Android Development in general and especially with Jetpack Compose and its ways of updating the Composables. I have a iOS background with lots of SwiftUI though.
Anyways, I have the following app
The Composables look like this:
#Composable
fun Greeting() {
Column(
modifier = Modifier
.fillMaxHeight()2
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
IncrementButton()
DecrementButton()
}
PressedText()
SizeText()
}
}
#Composable
fun PressedText() {
val myViewModel: MyViewModel = viewModel()
val myNumber by myViewModel.number.observeAsState()
Text(text = "Pressed: $myNumber")
}
#Composable
fun SizeText() {
val myViewModel: MyViewModel = viewModel()
val myList by myViewModel.list.observeAsState()
Text(text = "Size: ${myList?.size}")
}
#Composable
fun IncrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::add) {
Text("Add")
}
}
#Composable
fun DecrementButton() {
val myViewModel: MyViewModel = viewModel()
Button(onClick = myViewModel::remove) {
Text("Remove")
}
}
The view model I am using looks like this:
class MyViewModel : ViewModel() {
private val _number = MutableLiveData<Int>()
val number: LiveData<Int> = _number
private val _list = MutableLiveData<MutableList<Int>>()
val list: LiveData<MutableList<Int>> = _list
init {
_number.value = 0
_list.value = mutableListOf()
}
fun add() {
_number.value = _number.value?.plus(1)
_number.value?.let {
_list.value?.add(it)
_list.value = _list.value
}
}
fun remove() {
_number.value = _number.value?.minus(1)
if (_list.value?.isNotEmpty() == true) {
_list.value?.removeAt(0)
_list.value = _list.value
}
}
}
When I press the "Add"-button the number after "Pressed" gets updated but not the number after "Size".
Am really not sure about those lines with _list.value = _list.value that I have from some other SO post that said to update the reference of the list.
What am I missing? Any hints highly appreciated.
Feel free to leave any comments regarding code design.
Thank you!
This _list.value = _list.value is a really bad idea. Depending on underlying implementation, it may work or may not. In this case it's probably compared by pointer, that's why it doesn't trigger recomposition.
Check out Why is immutability important in functional programming.
The safe way is using non mutable list:
private val _list = MutableLiveData<List<Int>>()
And mutate it like this:
_list.value = _list.value?.toMutableList()?.apply {
add(value)
}
By doing this, you're creating a new list each time, and this will trigger recomposition without problems.
Also, using LiveData is not required at all: if you don't have some dependencies, which makes you using it, you can go for Compose mutable state: it's much cleaner:
var number by mutableStateOf(0)
private set
private val _list = mutableStateListOf<Int>()
val list: List<Int> = _list
fun add() {
number++
_list.add(number)
}
fun remove() {
number--
_list.removeAt(0)
}
I have a Composable and a viewmodel (VM) for it. The VM gets some data from a kotlin flow which I would like to expose as a State
Usually I would have the VM expose a state like this:
var title by mutableStateOf("")
private set
And I could use it in the Composable like this
Text(text = viewModel.title)
But since the data comes from a flow, i have to expose it like this
#Composable
fun title() = flowOf("TITLE").collectAsState(initial = "")
And have to use it in the Composable like this
Text(text = viewModel.title().value)
I try to minimize boilerplate code, so the .value kind of bothers me. Is there any way to collect the flow as state, but still expose it as viewModel.title or viewModel.title() and get the actual String and not the state object?
You can use delegated property.If your program just read it.
class FlowDeletedProperty<T>(val flow: Flow<T>, var initialValue: T, val scope: CoroutineScope) :
ReadOnlyProperty<ViewModel, T> {
private var _value = mutableStateOf(initialValue)
init {
scope.launch {
flow.collect {
_value.value = it
}
}
}
override fun getValue(thisRef: ViewModel, property: KProperty<*>): T {
return _value.value
}
}
fun <T> ViewModel.flowDeletedProperty(flow: Flow<T>, initialValue: T) =
FlowDeletedProperty(flow, initialValue, viewModelScope)
in viewModel
val a = flow {
while (true) {
kotlinx.coroutines.delay(100L)
println("out ")
emit((100..999).random().toString())
}
}
val title by flowDeletedProperty(a,"")
in ui
Text(text = viewModel.title)
The Code A is from offical sample project here.
The InterestsViewModel define uiState as StateFlow, and it is converted as State<T> by collectAsState() in the Composable function rememberTabContent.
I'm very strange why the author doesn't define uiState as State<T> directly in InterestsViewModel, so I write Code B.
The Code B can be compiled , and it can run, but it display nothing in screen, what is wrong with Code B ?
Code A
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
val loading: Boolean = false,
)
class InterestsViewModel(
private val interestsRepository: InterestsRepository
) : ViewModel() {
// UI state exposed to the UI
private val _uiState = MutableStateFlow(InterestsUiState(loading = true))
val uiState: StateFlow<InterestsUiState> = _uiState.asStateFlow()
...
init {
refreshAll()
}
private fun refreshAll() {
_uiState.update { it.copy(loading = true) }
viewModelScope.launch {
...
// Wait for all requests to finish
val topics = topicsDeferred.await().successOr(emptyList())
val people = peopleDeferred.await().successOr(emptyList())
val publications = publicationsDeferred.await().successOr(emptyList())
_uiState.update {
it.copy(
loading = false,
topics = topics,
people = people,
publications = publications
)
}
}
}
}
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
// UiState of the InterestsScreen
val uiState by interestsViewModel.uiState.collectAsState()
...
return listOf(topicsSection, peopleSection, publicationSection)
}
#Composable
fun InterestsRoute(
interestsViewModel: InterestsViewModel,
isExpandedScreen: Boolean,
openDrawer: () -> Unit,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
val tabContent = rememberTabContent(interestsViewModel)
val (currentSection, updateSection) = rememberSaveable {
mutableStateOf(tabContent.first().section)
}
InterestsScreen(
tabContent = tabContent,
currentSection = currentSection,
isExpandedScreen = isExpandedScreen,
onTabChange = updateSection,
openDrawer = openDrawer,
scaffoldState = scaffoldState
)
}
Code B
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
var loading: Boolean = false,
)
class InterestsViewModel(
private val interestsRepository: InterestsRepository
) : ViewModel() {
// UI state exposed to the UI
private var _uiState by mutableStateOf (InterestsUiState(loading = true))
val uiState: InterestsUiState = _uiState
...
init {
refreshAll()
}
private fun refreshAll() {
_uiState.loading = true
viewModelScope.launch {
...
_uiState = _uiState.copy(
loading = false,
topics = topics,
people = people,
publications = publications
)
}
}
}
#Composable
fun rememberTabContent(interestsViewModel: InterestsViewModel): List<TabContent> {
// UiState of the InterestsScreen
val uiState = interestsViewModel.uiState
...
return listOf(topicsSection, peopleSection, publicationSection)
}
The uiState that you are using in your Composable val uiState: InterestsUiState = _uiState is not a State and hence doesn't respond to changes. It's just a normal InterestsUiState initialized with the current value of _uiState.
To make it work, you can simply expose the getter for _uiState.
var uiState by mutableStateOf (InterestsUiState(loading = true))
private set
Now this uiState can only be modified from inside the ViewModel and when you use this in your Composable, recomposition will happen whenever the value of uiState changes.
The following code is from the project.
It seems that the project use Hilt to generate object automatically.
The class DetailsViewModel is the child class of ViewModel(), I think the paramater viewModel: DetailsViewModel in fun DetailsScreen() can be instanced automatically, but in fact it's assigned with viewModel: DetailsViewModel = viewModel(), why?
#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 -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
#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"))
}
}
}
When using Hilt, you should use hiltViewModel() instead of viewModel(): it creates an object with all injections or returns an object already created in the current scope.
Compose is not part of Hilt, so I don't know how you expect this object to be created without any call? hiltViewModel() is already very short and does all the work for you.
Passing the view model as a default argument is made for the convenience of testing and using #Preview: in the main application you do not pass this argument and let the default viewModel()/hiltViewModel() be called, but in a test call you can pass a simulated view model.
I'm learning Compose by the article.
A stateless composable is a composable that doesn't hold any state. An easy way to achieve stateless is by using state hoisting, so I replace Code B with Code A, it's great!
The article tell me:
By hoisting the state out of HelloContent, it's easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen, you don't have to change how HelloContent is implemented.
So I write Code C, it stores the value of name in a SharedPreferences, I think that Code C is just like Code A, but in fact, I can't input any letter with Code C, what wrong with the Code C ?
Code A
#Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
Code B
#Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Code C
#Composable
fun HelloScreen() {
var name: String by PreferenceTool( LocalContext.current ,"zipCode", "World")
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: default
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
Om is completely right about the reasons why your code doesn't work, and his answer will work.
To understand why you need a MutableState in compose I suggest you start with documentation, including this youtube video which explains the basic principles.
But PreferenceManager is deprecated and now you can use DataStore instead.
With compose in can be used like this:
#Composable
fun <T> rememberPreference(
key: Preferences.Key<T>,
defaultValue: T,
): MutableState<T> {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val state = remember {
context.dataStore.data
.map {
it[key] ?: defaultValue
}
}.collectAsState(initial = defaultValue)
return remember {
object : MutableState<T> {
override var value: T
get() = state.value
set(value) {
coroutineScope.launch {
context.dataStore.edit {
it[key] = value
}
}
}
override fun component1() = value
override fun component2(): (T) -> Unit = { value = it }
}
}
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
Usage:
var name by rememberPreference(stringPreferencesKey("zipCode"), "World")
One key piece is missing in Code C, which is MutableState.
In Compose the only way to modify content UI is by mutation of its corresponding state.
Your code doesn't have a mutable state object backing up PreferenceTool. So use of setValue by property delegation only modifies the SharedPreference (by calling putPreference(name, value)) but change is not propagated to UI.
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
In order to correct the behavior, add a MutableState object within PreferenceTool.
This way the updates are detected by Compose and UI is updated accordingly.
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private val state = mutableStateOf(findPreference(name, default))
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
state.value = value
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = { ... }
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = { ... }
}