Should state ever be exposed like this? ```privat...
# compose-android
c
Should state ever be exposed like this?
Copy code
private val _isInitialized = mutableStateOf(false)
    val isInitialized: State<Boolean> = _isInitialized
👀 1
t
Kotlin was not supposed to add something for that with a different type for the backing field ?
c
Why do you see a problem, @Colton Idle ?
e
the usual pattern is more like
Copy code
private val _isInitialized = mutableStateOf(false)
val isInitialized: State<Boolean>
    get() = _isInitialized
but it basically works the same
☝️ 1
☝🏻 1
c
e
w
You ideally shouldn't be exposing
State
at all. Or even using it. Just use regular
by mutableStateOf() private set
.
👎🏿 1
👎🏻 1
👎 2
j
Definitely use the
get
version, no need to waste bytes storing the same reference in two backing fields.
today i learned 2
c
Why do you see a problem, @Colton Idle ?
mostly because it's exposing
State
. I thought that was frowned upon?
Now that I think about it. I think the thing I'm thinking about was... "dont do this"
Copy code
@Composable
fun MyThing(name: State<String>)
👍🏻 1
👍🏾 1
t
Definitely use the
get
version, no need to waste bytes storing the same reference in two backing fields.
What "two backing fields" mean here? The mutable and the immutable one? What would be the difference of using the one from the get? Doesn't it invoke the original one and apply the immutability every time it's used?
z
This creates a new property, with a backing field, which still store the value it was initialized to (which happens to be the same reference that's already stored in
_isInitialized
). When you read it, it will read the stored value from its own backing field.
Copy code
val isInitialized: State<Boolean> = _isInitialized
This, on the other hand, creates a property with no backing field, so it's not initialized in any way when created. When it's read, its custom getter will read the value of
_isInitialized
and return that.
Copy code
val isInitialized: State<Boolean>
    get() = _isInitialized
t
So you're saying that in the first case the value from
isInitialized
can be different from the value in
_isInitialized
? Isn't it keeping reference for the same
mutableStateOf
?
z
It'll be the same value (same reference) returned either way, but it's stored twice in the first case. In the second case, it's only stored once, in
_isInitialized
.
gratitude thank you 1
today i learned 2
Since it's all
val
, the result here is the same, you're just wasting a bit of memory. If there were mutable variables, it could also lead to different results, such as
Copy code
class StoreTwice {
    var x = 1
    val y = x
}

class StoreOnce {
    var x = 1
    val y: Int get() = x
}

fun main() {
    val twice = StoreTwice()
    twice.x = 2
    println(twice.y) // 1

    val once = StoreOnce()
    once.x = 2
    println(once.y) // 2
}
d
I’ve stopped using backing fields and just expose the MutableState trusting accessors not to change it..maybe I’d use them for libraries but otherwise the extra boilerplate just isn’t worth it to me. Not saying it’s a best practice either, but I don’t like having an extra underscore version of a variable, just feels off/ugly. It would be nice if common mutable objects could use the “private set;” pattern
same 3
🫣 1
c
Yeah, I typically don't use a backing field either. Plus adam powell at some point sorta convinced me that its okay to change state of stuff in the composable itself instead of bouncing back into the VM. I think "composable" and "view model" are both "view layer" and so its fine to modify stuff there. idk /shruggie
z
in an app it probably doesn't matter as much, but in a library i would avoid exposing
MutableState
in these kinds of cases. And in a library, avoiding
State
too if possible to reduce the API footprint. Exceptions would be helpers like
collectAsState
where you explicitly want your callers to be able to use property delegation.
❤️ 1
c
thanks everyone for teaching ❤️
d
I'll just remind of the case where using a backing field is justified - exposing public
StateFlow
from a
MutableStateFlow
, especially on public library code that you want to somewhat harden against abuse Don't do
Copy code
val _someFlow = MutableStateFlow(someInitialT)
val someFlow: StateFlow<T> get() = _someFlow
Do
Copy code
val _someFlow = MutableStateFlow(someInitialT)
val someFlow: StateFlow<T> = _someFlow.asStateFlow() // <= Light wrapping prevents casting back to 'Mutable'
j
Hard disagree
Who is casting things back to mutable? This hypothetical person doesn't exist
😂 2
z
If people are downcasting types like this you have an entirely different sort of problem, I also never understood using
asStateFlow
d
Hmm, it's a fair point. I've been in a position of building a proprietary SDK before where hardening in all sorts of ways was a concern, so we used this. But in normal cases, nobody's trying to do that.
e
regardless of the hypothetical downcasting caller, if the wrapper is light-weight, then there's no need to put it in a backing field either. just use
get() = asStateFlow()
t
In French we say: "Avec des si, on mettrait Paris en bouteille"
💡 1
👍 1
z
Also, your caller can still reflectively get the mutable flow from the field inside whatever
asStateFlow()
returns. Language features can never secure you from stuff running in the same process. If you are worried about your consumer developers being malicious, you need a process boundary not a library.
regardless of the hypothetical downcasting caller, if the wrapper is light-weight, then there's no need to put it in a backing field either. just use
get() = asStateFlow()
That will mess with any compose code calling
someFlow.collectAsState()
since it will cancel and restart collection every time due to the new instance.