Usually, compose codes are like
#Preview
#Composable
fun BuildMyView() {
val counter = rememberSaveable { mutableStateOf(1) }
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
.clickable { counter.value++ },// click and text will ++
text = "${counter.value}"
)
}
Recently, I want to collect all view data together and build a state and create sth like:
data class MyState(
val data: MutableState<String>
)
val stateTemp = MyState(mutableStateOf("hello"))
#Preview
#Composable
fun BuildMyView() {
val counter = rememberSaveable { stateTemp.data}
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
.clickable { stateTemp.data.value += "-1" },
text = counter.value
)
}
In second case, when I click text, it does ++ but if I go to a new page and come back, all changes will lost however in first case it doesn't.
I then read some compose codes and get confusing since I didn't find where remember subscribe a mutable state.
Is there a method to make mutable state out of compose work?
Beside, is somewhere I can find codes generated by compose under my gradle build dir or anywhere else (except dex, that's too hard to read)? Compose really did amazing job but I cannot read the real codes running and that makes much more difficult for freshman to get start.
UPDATE ON 2022/6/15
Now I found a proper solution to use viewmodel instead of state holder and use mutableState in viewmodel and subscribe state in view composable part so that I can avoid complex grammar of viewmodel with livedata. Hope that will help followers.
I then read some compose codes and get confusing since I didn't find where remember subscribe a mutable state.
As far as I understand, remember calculates and caches what ever is inside its lambda during the first composition or first execution of the #composable function only, this is the only thing that the #composable remembers or subscribes on. Unless you provide a key to a remember, that when it changed, it will trigger a re-calculation to be remembered
Is there a method to make mutable state out of compose work?
Do you mean some piece of non composable code or function that will automatically trigger when this State object is changed? If this is what you mean, I suppose you just have to resort back to good old observable patterns
As far as I understand, compose States are specifically designed to work with the underlying Snapshot system where the heart of triggering the composition mechanism happens, I can't imagine (so far) any usage of State objects outside of composition or outside of the Snapshot system.
In second case, when I click text, it does ++ but if I go to a new page and come back, all changes will lost however in first case it doesn't.
As for using rememberSaveable, I would be careful using it , it might look the same as remember with just an additional power of saving/restoration, but it has more power with equal responsibilities imposed to it that you have to take into account when using it. I haven't used or defined a rememberSaveable object without defining a Saver in it explicitly, so I can only assume in your case here
rememberSaveable { mutableStateOf(1) }
the composable observes a rememberSaveable that also observes an actual object that actually changes and implicitly saves and restores that object with the most recent value
while in this, I think
data class MyState(
val data: MutableState<String>
)
val stateTemp = MyState(mutableStateOf("hello"))
...
...
rememberSaveable { stateTemp.data}
rememberSaveable saves the state of the val data: MutableState<String> (which is empty), while the composable observes an instance of a mutableState that is changing, unfortunately rememberSaveable already saved an initial state in way like this rememberSaveable { data: MutableState<String> } not the actual mutableStateOf("hello") that changes, so it will restore it that way when you go back.
I'm curious, you can try implementing your MyState class with a companion object holding a Saver where you can define how rememberSaveable will Save and Restore the data, I think it will restore it when you navigate back to that screen. When you debug a Saver implementation, you will also notice that the restoration is invoked during recomposition. Im not quite sure though
Disclaimer: Im just learning compose recently and still digging deeper about the Snapshot system, and it seemed like you're heading in the same direction as I do. I'd recommend to visit this link once in a while if your'e interested in the Snapshot system. Apologies as well, I can't comment yet due to lack of reputation so I just posted my thought and current understanding of how State and composition work together.
Related
I have an activity which displays multiple fragments depending on which one is selected.
I also have a button in this activity and I want to obtain a value from a specific fragment when this button is clicked.
How can I obtain this value?
I tried to get the view I wanted from the fragment from the activity as the code shows below but I can understand that it doesn't work since the fragment is still to be created.
onOffButton.setOnClickListener {
if (onOffButton.text.contains("ON")) {
onOffButton.text = "TURN OFF"
var hoursPicker = findViewById<NumberPicker>(R.id.hoursPicker)
}
}
The short version is you shouldn't do this, there are all kinds of complications (especially when you're trying to access the Fragment's Views).
It's even more complicated if the Fragment might not even be added to the UI at all! If it's not there, what value are you supposed to use? If you want to somehow create the Fragment just so it exists, and so you can read the value from its text box, then that's a sign the value really needs to be stored somewhere else, so you don't need the Fragment if you want to access it.
The easiest, recommended, and modern way to share data like this is with a ViewModel:
class MyViewModel : ViewModel() {
// setting a default value here!
var currentHour: Int = 0
}
class MyActivity : AppCompatActivity() {
val model: MyViewModel by viewModels()
fun onCreate(...) {
...
onOffButton.setOnClickListener {
// access the data in the ViewModel
val currentHour = model.currentHour
}
}
}
class MyFragment : Fragment() {
// using activityViewModels so we get the parent Activity's copy of the VM,
// so we're all sharing the same object and seeing the same data
val model: MyViewModel by activityViewModels()
fun onViewCreated(...) {
...
hoursPicker.setOnValueChangeListener { _, _, newValue ->
// update the VM
model.currentHour = newValue
}
}
}
So basically, you have this ViewModel object owned by the Activity and visible to its Fragments. The VM outlives all of those components, so you don't lose data while an Activity is being destroyed on rotation, or when a Fragment isn't added to the UI, etc.
The VM is the source of data, everything else just reads from it, or updates it when something changes (like when the Fragment updates the variable when its number picker's value changes). This way, the Activity doesn't need to go "ask" the Fragment for info - it's stored in a central location, in the VM
This is the most basic way to use a ViewModel - you can start using LiveData and Flow objects to make different UI components observe data and react to changes too. For example, your button in your Activity could change some enabled state in the VM, and the Fragment (if it's added) will see that change and can do things like make the number picker visible or invisible.
It's way easier to coordinate this stuff with a ViewModel, so if you don't already know how to use them, I'd recommend learning it!
I'm creating a Row of Text Composables, imagine it like a table row.
Image: https://i.stack.imgur.com/jR3oB.png
If you care for the backstory:
The amount of "cells" of my custom Row Composable is dynamic and I use Box Composables to wrap the Text in the "cells". I need to apply different Modifier parameters to each Box, which I first tried to achieve via creating a Modifier extension.
Code of my Modifier extension:
#SuppressLint("ComposableModifierFactory", "ModifierFactoryExtensionFunction") // I wish I could. The RowScope requirement makes doing it the easy way impossible
#Composable
fun RowScope.rowOfTextCellsBoxModifier(
index: Int,
columnWidths: ArrayList<Dp?>,
columnWeights: ArrayList<Float?>
): Modifier {
#Suppress("RedundantExplicitType") // bull**** (compose kotlinCompilerExtensionVersion 1.3.2)
var returnModifier: Modifier = Modifier
if (index != columnWidths.size - 1)
returnModifier = returnModifier.then(Modifier.verticalEndLine())
returnModifier = if (columnWidths[index] != null)
returnModifier.then(Modifier.width(columnWidths[index]!!))
else {
val weight = try {
columnWeights[index]
} catch (ignored: Throwable) {
1f
}
returnModifier.then(Modifier.weight(weight!!))
}
return returnModifier
}
I moved on to just generating the entire Box in a #Composable function, but that's beside the point ;)
As you can see, I want to add a Modifier.weight() at one point.
However, .weight() is a Modifier function that is only applicable in certain Scopes, like in RowScope. Which means that my "Modifier extension" needs to apply to RowScope, which turned out impossible to find instructions on.
So, imagine I hadn't found a better solution:
What would be the proper way of extending the RowScope Interface's Modifier?
Usually, when you need to create a Composable that is applicable to certain scopes, you write something like:
RowScope.YourComposable() {...}
And when you extend Modifier, you write:
Modifier.yourExtension() {...}
But that does not work for Scoped Modifiers:
RowScope.Modifier.YourModifierExtension() {...}
is invalid code.
This android tutorial has the below code snippet:
#Composable
private fun MyApp() {
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
My first thought was that the Surface composable gets its Greeting child by calling the lambda parameter containing the child and getting it back as a return value. But later in the tutorial we get this example:
#Composable
private fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = "Hello,")
Text(text = name)
}
}
}
Somehow the Column is aware of both the "Hello" Text and the name Text, even though calling the lambda would only give the name Text as a return value.
So my question is: what is the mechanism that makes a Composable aware of its children?
Similarly to for example suspend functions, #Composable functions are processed by the compiler in a very special way. This is to allow automatic recompositions and also to implicitly pass the context between components.
Documentation for #Composable specifies:
A useful mental model for Composable functions is that an implicit "composable context" is passed into a Composable function, and is done so implicitly when it is called from within another Composable function. This "context" can be used to store information from previous executions of the function that happened at the same logical point of the tree.
We can see it in action by writing a simple composable function and analyzing the resulting bytecode. Taking this source code:
#Composable
fun Foo() {
Text("foo")
}
We get the bytecode consisting of hundreds of instructions and some of the resulting code is even placed in a separate, synthesized class. We will focus on most important parts:
public static final void Foo(androidx.compose.runtime.Composer, int);
As we can see, our parameterless Foo() function actually implicitly receives a Composer and some integer.
3: invokeinterface #24, 2 // InterfaceMethod androidx/compose/runtime/Composer.startRestartGroup:(I)Landroidx/compose/runtime/Composer;
Another Composer object is acquired by calling startRestartGroup() on the received Composer.
55: invokestatic #73 // Method androidx/compose/material/TextKt."Text-fLXpl1I":(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextAlign;JIZILkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;III)V
This is a call to Text(). It is hard to read due to the large number of optional parameters, but we can notice that it also receives a Composer object. This parameter is synthesized, we can't find it in the list of Text() parameters - similarly as in our Foo() function.
Foo() passes the composer acquired with startRestartGroup() to Text().
While I don't know the exact functionality and the meaning of the Composer, we can clearly see that Compose framework implicitly passes the context between composable functions, making possible to wire components together.
The Code A is from a official sample project.
1: The author define all events such as onAddItem, onRemoveItem ... in the entrance UI fun TodoScreen, is it a good design?
2: You know the fun TodoScree will become huge when the functionality of the APP is increased, how can I improve the app architecture ?
Code A
#Composable
fun TodoScreen(
items: List<TodoItem>,
currentlyEditing: TodoItem?,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit,
onStartEdit: (TodoItem) -> Unit,
onEditItemChange: (TodoItem) -> Unit,
onEditDone: () -> Unit
) {
...
}
class TodoActivity : AppCompatActivity() {
val todoViewModel by viewModels<TodoViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StateCodelabTheme {
Surface {
TodoActivityScreen(todoViewModel)
}
}
}
}
}
#Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
TodoScreen(
items = todoViewModel.todoItems,
currentlyEditing = todoViewModel.currentEditItem,
onAddItem = todoViewModel::addItem,
onRemoveItem = todoViewModel::removeItem,
onStartEdit = todoViewModel::onEditItemSelected,
onEditItemChange = todoViewModel::onEditItemChange,
onEditDone = todoViewModel::onEditDone
)
}
This design pattern is called state hoisting, and is explained in the docs:
https://developer.android.com/jetpack/compose/state#state-hoisting
State hoisting in Compose is a pattern of moving state to a
composable's caller to make a composable stateless. State that is
hoisted this way has some important properties:
Single source of truth: By moving state instead of duplicating it, we're ensuring there's only one source of truth. This helps avoid
bugs. Encapsulated: Only stateful composables will be able to modify
their state. It's completely internal.
Shareable: Hoisted state can be shared with multiple composables. Say we wanted to name in a different composable, hoisting would allow
us to do that.
Interceptable: callers to the stateless composables can decide to ignore or modify events before changing the state.
Decoupled: the state for the stateless ExpandingCard may be stored anywhere. For example, it's now possible to move name into a
ViewModel.
About the issue with "my screen constructor is gonna be huge".
For lower level components i like to follow the pattern described in this compose tutorial: https://youtu.be/SMOhl9RK0BA?t=546 : don't pass five different text with five onClickLambdas for five different buttons to a component, just pass in a list of 5 Buttons
For higher level components i like to make sure i only pass high level stuff, and let the composable figure out the low level things.
I've been having a shot at kotlin multiplatform and it's brilliant, but threading stumps me. The freezing of state between threads makes sense conceptually, and works fine in simple examples where small objects or primitives are passed back and forth, but in real world applications I can't get around InvalidMutabilityException.
Take the following common code snippet from an android app
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
private var coreroutineSupervisor = SupervisorJob()
private var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main + coreroutineSupervisor)
private fun loadResults() {
// Here: Show loading
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) { objectWhichContainsNetworking.fetchData() }
// Here: Hide loading and show results
} catch (e: Exception) {
// Here: Hide loading and show error
}
}
}
Nothing very complex, but if used in common code and run from Kotlin/Native then pow InvalidMutabilityException on MainViewModel.
It seems the reason for this is that anything passed in withContext is frozen recursively so because objectWhichContainsNetworking is a property of MainViewModel and is used in withContext then MainViewModel gets caught in the freeze.
So my question is, is this just a limitation of the current Kotlin/Native memory model? Or perhaps the current version of coroutines? And are there any ways round this?
Note: coroutines version: 1.3.9-native-mt. kotlin version 1.4.0.
Edit 1:
So it appears that the above slimmed down code actually works fine. It turns out the incriminating code was an updateable var in the view model (used to keep a reference to the last view state) which becomes frozen and then throws the exception when it tries to be mutated. I'm going to make an attempt of using Flow/Channels to ensure there's no var reference needed and see if this fixes the overall problem.
Note: if there is a way to avoid MainViewModel being frozen in the first place it would still be fantastic!
Edit 2:
Replaced the var with Flow. I couldn't get standard flow collecting in iOS until using the helpers here: https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt.
MainViewModel still gets frozen, but as all it's state is immutable it's no longer a problem. Hope it helps someone!
In your original code, you are referencing a field of the parent object, which causes you to capture the whole parent and freeze it. It is not an issue with coroutines. Coroutines follows the same rules as all the other concurrency libraries in Kotlin/Native. It freezes the lambda when you cross threads.
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
//yada yada
private fun loadResults() {
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) {
//The reference to objectWhichContainsNetworking is a field ref and captures the whole view model
objectWhichContainsNetworking.fetchData()
}
} catch (e: Exception) {}
}
}
To prevent this from happening:
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
){
init{
ensureNeverFrozen()
}
//Etc
The most complicated thing with the memory model is this. Getting used to what's being captured and avoiding it. It's not that hard when you get used to it, but you need to learn the basics.
I've talked about this at length:
Practical Kotlin/Native Concurrency
Kotlin Native Concurrency Hands On
KotlinConf KN Concurrency
The memory model is changing, but it'll be quite a while before that lands. Once you get used to the memory model, the immutable issues are generally straightforward to diagnose.