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!
Related
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.
Reasoning:
Hello guys. I'm building an evolution simulator as personal project. I have a few parameters set on textfields, such as the speed of the simulation and the number of "organisms". These are going to be accessed by multiple components of the application. Because i would also like to use validation on a few parameters, I set up a ViewModel like such:
class ParametersModel: ViewModel() {
// These properties would likely be DoubleProperty, but for simplicity lets pretend they are strings
val simulationSpeed = bind { SimpleStringProperty() }
val organismsGenerated = bind { SimpleStringProperty() }
}
... and then perform the validation tests on the textfields:
val model = ParametersModel()
textfield(model.simulationSpeed).required()
This works alright, but the issue with it is that I'm defining the model properties as a bind to an empty SimpleDoubleProperty, which is redundant since I'm never commiting this model (the program should always read changes as they are typed). At the same time, I cant define the model properties as simply:
class ParametersModel: ViewModel() {
val simulationSpeed = SimpleStringProperty()
val organismsGenerated = SimpleStringProperty()
}
Because I then get an error about the validation:
The addValidator extension can only be used on inputs that are already bound bidirectionally to a property in a Viewmodel. Use validator.addValidator() instead or make the property's bean field point to a ViewModel.
The other option I could take would be to make a class named something like GlobalProperties, which would keep my properties and also a ValidationContext. I could then add validators by using validationContext.addValidator and pass the textfields. But at this point I feel I'm just coding a ViewModel equivalent.
Question:
Is ViewModel the correct way of keeping "globally" accessed parameters set by textfields? If so, is there a way to not have to set the model properties as a bind of an empty one, since i dont ever need to commit anything?
Usually you would use a ViewModel with some sort of model. Then you can use the ViewModel to handle user input, which stores the current state of the user input, and the backing model will only be update when the ViewModel is committed, assuming validation passes (which seems at odds with your claim that you "dont ever need to commit anything").
Something like this:
class Parameters {
val simulationSpeedProperty = SimpleStringProperty(...)
var simulationSpeed by simulationSpeedProperty
val organismsGeneratedProperty = SimpleStringProperty(...)
var organismsGenerated by organismsGeneratedProperty
}
class ParametersModel(parameters: Parameters): ItemViewModel<Parameters>(parameters) {
val simulationSpeed = bind(Parameters::simulationSpeedProperty)
val organismsGenerated = bind(Parameters::organismsGeneratedProperty)
}
Then you can be sure that the Parameters backing the ParametersModel always has valid values in it (assuming of course it was initialized with valid values).
Im trying to get some data out of other ViewModels inside another ViewModel to make my code smaller, but im having a problem trying to implement what already worked on a fragment or in a activity, this is what i got:
class ObraConMediaViewModel(private val context: ViewModelStoreOwner,
private val id: Int): ViewModel(), LifecycleObserver {
var allObras: LiveData<ArrayList<ObraConMedia>>
private lateinit var viewModelobras: ViewModelObras
private lateinit var viewModelMediaObra: ViewModelMediaObra
val repositoryobras =ObrasRepository()
val repositoryMediaObra = MediaObraRepository()
val viewModelFactoryobras = ViewModelFactoryObras(repositoryobras)
val viewModelMediaObraFactory = ViewModelMedIaObraFactory(repositoryMediaObra)
init{
viewModelobras = ViewModelProvider(context, viewModelFactoryobras)
.get(ViewModelObras::class.java) // requireActivity() when called
viewModelMediaObra = ViewModelProvider(context, viewModelMediaObraFactory)
.get(ViewModelMediaObra::class.java)
viewModelobras.getObras(id)
viewModelobras.myResponse.observe(this , Observer { response ->
if (response.isSuccessful){
Log.d("Response", response.body()?.ans?.get(0)?.autor)
Log.d("Response", response.body()?.ans?.get(1)?.autor)
}else{
Log.d("Response", response.errorBody().toString())
}})
viewModelMediaObra.getMediaObra(Constantes.PRUEBA_ID)
viewModelMediaObra.myResponse.observe(this, Observer { response ->
if (response.isSuccessful){
Log.d("Response", response.body()?.ans?.get(0)?.filePath)
}
})
}}
I was having trouble with the Observer but extending the class to LifecycleObserver fixed it, i have no idea if this will even work but the only error that i have right now its the owner of the .observe(this,....), i dont seem to find a way to pass a lifecycleowner from the fragment to this viewmodel. All the variables i need to make this viewmodel work are inside those two responses. If this is a very bad way to do it please tell me. Thanks for reading.
Kindly note that above approach is not correct.
One should not create a instance of ViewModel inside another ViewModel.
There is a possibility that one ViewModel may get destroyed before another. This will lead to garbage reference and memory leaks.
I would recommend you to create the instance of both View Models in an Activity/Fragment and then call respective methods of ViewModel from Activity/Fragment.
Also, as you want to make your code smaller and concise, I highly recommend you Shared ViewModel.
This Shared ViewModel can be used by two fragments.
Please refer to this link
I have stumbled upon a behaviour in TornadoFx that doesn't seem to be mentioned anywhere (I have searched a lot) and that I'm wondering about.
If I define a view like this with the TornadoFx builders for the labels:
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = label("second label")
}
The result is:
That is, the mere definition of secondLabel automatically adds it to the rootof the scene.
However, if I place this definition BEFORE the definition of root...
class ExampleView: View() {
val secondLabel = Label("second label")
override
val root = vbox{ label("first label") }
}
... or if I use the JavaFx Labelclass instead of the TornadoFx builder ...
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = Label("second label")
}
... then it works as I expect:
Of course, I can simply define all variables in the view before I define the rootelement but I'm still curious why this behaviour exists; perhaps I am missing some general design rule or setting.
The builders in TornadoFX automatically attach themselves to the current parent in the scope they are called in. Therefore, if you call a builder function on the View itself, the generated ui component is automatically added to the root of that View. That's what you're seeing.
If you really have a valid use case for creating a ui component outside of the hierarchy it should be housed in, you shouldn't call a builder function, but instead instantiate the element with it's constructor, like you did with Label(). However, the use cases for such behavior are slim to none.
Best practice is to store value properties in the view or a view model and bind the property to the ui element using the builders. You then manipulate the value property when needed, and the change will automatically update in the ui. Therefore, you very very seldom have a need to access a specific ui element at a later stage. Example:
val myProperty = SimpleStringProperty("Hello world")
override val root = hbox {
label(myProperty)
}
When you want to change the label value, you just update the property. (The property should be in an injected view model in a real world application).
If you really need to have a reference to the ui element, you should declare the ui property first, then assign to it when you actually build the ui element. Define the ui property using the singleAssign() delegate to make sure you only assign to it once.
var myLabel: Label by singleAssign()
override val root = hbox {
label("My label) {
myLabel = this
}
}
I want to stress again that this is very rarely needed, and if you feel you need it you should look to restructure your ui code to be more data driven.
Another technique to avoid storing references to ui elements is to leverage the EventBus to listen for events. There are plenty of examples of this out there.
So, I have a case in which I need to have N rows in form of: Label TextView/Checkbox. Maybe I will have to have more than those two views, so I want to be able to support anything that is TornadoFx View.
I've created an interface that has one method that returns TornadoFx View and it looks like this:
interface ValueContainer {
fun getView() : View
}
One of the implementations looks like this:
class BooleanValueContainer(val checked: Boolean) : ValueContainer {
val valueProperty = SimpleBooleanProperty(checked)
override fun getView(): View {
return (object : View() {
override val root = checkbox {
bind(valueProperty)
}
})
}
}
Now, when I try to use it inside init block, it doesn't show in the layout. root is GridPane and parameters is a list of objects that have name and reference to ValueContainer implementation (BooleanValueContainer or other one which I haven't shown):
init {
with(root) {
parameters.map {
row(it.name) {
it.parameterContainer.getView()
}
}
}
}
I'm stuck here for quite a while and I've tried anything I could find but nothing really worked except putting textview or checkbox block instead of getView() call, but then I would have to have logic on what view should I show inside this class which represents a view and I don't want that.
The reason this is not working for you is that you simply call parameterContainer.getView() but you don't add the View to the row. I think what's confusing you is that for builders you can just say label() for example, and it's added to the current Node in the builder tree. In your case, you just say Label() (just create an instance of Label, not call the label builder), which would create a new Label, but not add it to the children list of the current Node. To solve your problem, do:
this += it.parameterContainer.getView()
This will add the View to the row.
Apart from this, I don't quite see the point of the ValueContainer. What does it solve to put a View inside this container object? I suspect this as well might be due to a misunderstanding and I'd like to understand why you feel that you need this construct.