Yes, that's true. Copy is indeed only a shallow copy. I didn't really mention it because it and the different ways of emitting view state wasn't the focus of the article.
Having said that, I wholeheartedly disagree about using a single class to define view state. I find it to be very beneficial. Also, if the view model isn't combining it into single value the view itself has to effectively do that so you might as well keep things clean. (And if the view isn't combining it then you have an unknown number of combinations and permutations of your view state, which to me is sloppy.)
However, your comment on deep vs shallow copies is very valid. I would argue that if you really want to do a deep modification of an object within the view state directly you’ve got larger structural issues. Exposing multiple components of the view state directly just hides those issues.
It's for that reason that I almost never have a mutable view state. Instead I build my view state as a combination of other flows where each flow defines a single component of the view state.
(I'm not sure how well this code will copy into a comment reply but here goes.)
For example:
val viewState = flowA
.combine(flowB) { a, b -> ViewState(a = a, b = b) }
.combine(flowC) { newViewState, c -> newViewState.copy(c = c) }
.combine(flowD) { newViewState, d -> newViewState.copy(d = d) }
.stateIn(
scope = this.viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = ViewState()
)
Each component of the view state is itself a flow within the view model. They combine to produce the single emitted value. Need to update part of the view state? Emit a value down its component flow. If you want to update just a portion of that component, ie. a “deep” edit? Then it’s up to you to figure that out, maybe you copy the current value and edit some low level property. Either way, you still have to emit a value down the component flow where it combines to build the new view state.
It completely avoids the need to worry about race conditions, mutexes, shallow vs deep copies, etc. Heck, the component flows can be calculated on their own threads too if thats your thing. It even rebuilds from a saved state handle very easily.