how to initialise ViewModel in createFragment of FragmentStateAdapter? - android-livedata

I have a ExerciseDetailFragment that contains an ExerciseFragmentStatePagerAdapter to show a bunch of SupportUsFragmentCard. These cards can be swiped horizontally by the user.
In my ExerciseFragmentStatePagerAdapter I have the following code:
fun createFragment(position: Int): Fragment {
val exercise = exercises[position]
val card = SupportUsFragmentCard()
card.setExercise(exercise) <---- this is my question/problem
return card
}
As you can see the pager adapter just instantiates the SupportUsFragmentCard and for this the exercise needs to be passed along (the card displays some exercise information) via the setExercise.
The SupportUsFragmentCard looks like this:
open class SupportUsFragmentCard : RootFragment() {
...
val viewModel: SupportUsViewModel by viewModels()
...
fun setExercise(exercise: Exercise?) {
viewModel.setExercise(exercise) <---- passes on the exercise to the viewModel
}
It’s implementation passes along the exercise to the underlying viewModel. Which in its turn encapsulates this into a LiveData (on which the SupportUsFragmentPlain has observers but I omitted this code as its not the issue):
class SupportUsViewModel() : ViewModel() {
//interface to the view
fun getExercise(): LiveData<Exercise?> = exercise
fun setExercise(execise: Exercise?) {
exercise.value = execise. <--- updates the Livedata object
}
//underlying live data mutable values
private val exercise = MutableLiveData<Exercise?>(null)
}
When this code is executed, it crashes with exception:
IllegalStateException: Can't access ViewModels from detached fragment
I think because the createFragment ends up updating the viewModel without actually already being on screen. I also feel that this way of working does not respect the MVVM architecture.
What is the correct way in this scenario, using MVVM and LiveData to initialise the SupportUsViewModel with an Exercise in the ExerciseFragmentStatePagerAdapter's createFragment function?

Related

Updating a #State var in SwiftUI from an async method doesn't work

I have the following View:
struct ContentView: View {
#State var data = [SomeClass]()
var body: some View {
List(data, id: \.self) { item in
Text(item.someText)
}
}
func fetchDataSync() {
Task.detached {
await fetchData()
}
}
#MainActor
func fetchData() async {
let data = await SomeService.getAll()
self.data = data
print(data.first?.someProperty)
// > Optional(115)
print(self.data.first?.someProperty)
// > Optional(101)
}
}
now the method fetchDataSync is a delegate that gets called in a sync context whenever there is new data. I've noticed that the views don't change so I've added the printouts. You can see the printed values, which differ. How is this possible? I'm in a MainActor, and I even tried detaching the task. Didn't help. Is this a bug?
It should be mentioned that the objects returned by getAll are created inside that method and not given to any other part of the code. Since they are class objects, the value might be changed from elsewhere, but if so both references should still be the same and not produce different output.
My theory is that for some reason the state just stays unchanged. Am I doing something wrong?
Okay, wow, luckily I ran into the Duplicate keys of type SomeClass were found in a Dictionary crash. That lead me to realize that SwiftUI is doing some fancy diffing stuff, and using the == operator of my class.
The operator wasn't used for actual equality in my code, but rather for just comparing a single field that I used in a NavigationStack. Lesson learned. Don't ever implement == if it doesn't signify true equality or you might run into really odd bugs later.

Jetpack Compose LazyColumn - How to update values of each Item seperately?

I'm working on a shopping cart feature for my app. I'm looking to add/decrease quantity of each list item in LazyColumn individually. I'm using only one "remember" so if I click on add/decrease they all update at the same time. How do I control each Item individually?
Screenshot
#Composable
fun InventoryCartScreen(
mainViewModel: MainViewModel = hiltViewModel()
) {
val multiSelectValue = mutableStateOf(0)// This is the value I want to change
//random list
val shopList = listOf(
ShoppingList(id = 0,itemNumber = "1",itemDescription = "1",currentInventory = 0,optimalInventory = 0,minInventory = 0),
ShoppingList(id = 0,itemNumber = "2",itemDescription = "2",currentInventory = 0,optimalInventory = 0,minInventory = 0)
)
Column(...) {
LazyColumn(...) {
items(items = shopList, key = { it.id }) { item ->
InventoryCartScreenContents(
onaddClick= { multiSelectValue.value ++ }, //adds for all
onDecreaseClick = { multiSelectValue.value -- }, //decreases for all
value = multiSelectValue.value //setting the initial value for all
)
}
}
}
}
Below is the contents composable to help you reproduce the issue.
#Composable
fun InventoryCartScreenContents(
onAddClick: (Int) -> Unit,
onDecreaseClick: () -> Unit,
value: Int,
) {
Row(...) {
Button(
onClick = { onAddClick(itemId) }
) {
Text(text = "+")
}
Button(
onClick = onDecreaseClick
) {
Text(text = "-")
}
}
}
Create a mutableStateListOf(...) (or mutableStateOf(listOf(...)) object if the former does not support your data type) in your ViewModel. Now, access this state from the composable you wish to read it from, i.e., your LazyColumn.
Inside your ViewModel, you can set the values however you wish, and they will be updated in the Composable as and when they are updated. Now, the composable, i.e., you column could use the index of the list item as the index of the lazycolumn's item. So you can store different data for different items in the viewmodel and it'll work fine.
The problem seems to be that you are missing the concept of state-hoisting here. I thought I had some good posts explaining it but it seems this one's the best I've posted. Anyway, I recommend checking this official reference, since that's where I basically found that (with a little bit of headhunting, so to speak.)
The basic idea is that everything is stored in the viewModel. Then, you divide that state into "setters" and "getters" and pass them down the heirarchy.
For example, the ViewModel may have a item called text, ok?
class VM : ViewModel(){
var text by mutableStateOf("Initial Text")
private set // This ensures that it cannot be directly modified by any class other than the viewmodel
fun setText(newText: Dtring){ // Setter
text = newText
}
}
If you wish to update the value of text on the click of a button, this is how you will hook up that button with the viewModel
MainActivity{
onCreate{
setContent{
StateButton(
getter = viewModel.text, // To get the value
setter = viewModel::setText // Passing the setter directly
)
}
}
}
In your Button Composable declaration
#Composable
private fun ComposeButton(
getter: String,
setter: (String) -> Unit // (receive 'String') -> return 'Unit' or 'void'
){
Button(
onClick = { setter("Updated Text") } // Calls the setter with value "Updated Text", updating the value in the viewModel
){
Text(text = getter)
}
}
This button reads the value 'get' from the viewModel, i.e., it reads the value of text, as it is passed down the model to the composable. Then, when we receive a click event from the button (in the onClick), we invoke the setter that we received as a parameter to the Composable, and we invoke it with a new value, in this case "Updated Text", this will go upwards all the way to the viewModel, and change the value of text in there.
Now, text was originally initialized as a state-holder, by using mutableStateOf(...) in initialization, which will trigger recompositions upon its value being changed. Now, since the value actually did change (from "Initial Text" to "Updated Text"), recompositions will be triggered on all the Composables which read the value of the text variable. Now, our ComposeButton Composable does indeed read the value of text, since we are directly passing it to the getter parameter of that Composable, that right? Hence, all of this will result in a Composable, that will read a value from a single point in the heirarchy (the viewmodel), and only update when that value changes. The Viewmodel, therefore, acts as a single source of truth for the Composable(s).
What you'll get when you run this is a Composable that reads "Initial Text" at first, but when you click it, it changes to "Updated Text". We are connecting the Composables to the main viewModel with the help of getters and setters. When Composables receive events, we invoke setters we receive from the models, and those setters continue the chain up to the viewModel, and change the value of the variables (state-holder) inside the model. Then, the Composables are already reading those variables through 'getters', hence, they recompose to reflect the updated value. This is what state-hoisting is. All the state is stored in the viewModel, and is passed down to the Composables. When a value needs to change, the Composables pass 'events' up to the viewModel (up the heirarchy), and then upon the updating of the value, the updated state is passed down to the Composables ('state' flows down the heirarchy).
All you need, is to use this method, but with a list. You can keep track of the items by using their index, and update their properties in the viewModel like this example demonstrates updating the value of a. You can store all the properties of an item in a single list.
Just create a data-class, like so
data class Item(p1: ..., p2:..., p3 : ...){}
Then, val list by mutableStateOf(listOf<Item>())
Clear?
Ok here is the explanation SPECIFIC to your use-case.
Your code seems excessively large but here's what I got down:
You have two items in a lazycolumn, both of them have three buttons each. Your question is about two buttons, increase and decrease. You wish to have the buttons modify the properties of only the item they belong to, right?
Ok so again, use state-hoisting as explained above
ViewModel{
val list by mutableStateOf(listOf<Item>()) // Main (central) list
val updateItem(itemIndex: Int, item: Item){
list.set(itemIndex, item) // sets the element at 'itemIndex' to 'item'
} // You can fill the list with initial values if you like, for testing
}
Getters and Setters being created, you will use them to read and update ENTIRE ITEMS, even if you have to modify a single property of them. We can use convenience methods like copy() to make our lives easier here.
MainScreen(){
//Assuming here is the LazyColumn
list = viewModel.list // Get the central list here
LazyColumn(...){ // Try to minimize code like this in your questions, since it does not have to compile, just an illustration.
items(list){ item ->
// Ditch the multiSelectValue, we have the state in the model now.
Item( // A Composable, like you demonstrated, but with a simpler name for convenience
item: Item,
onItemUpdate: (Item) -> Unit // You don't need anything else
)
}
}
}
Just create a data class and store everything in there. The class Item (not the Composable) I've demonstrated is the data-class. It could have a value like so
data class Item(var quantity: Int, ...){} // We will increase/decrease this
Now, in your Item Composable, where you receive the 'add' event (in the onClick of the 'Add' Button), just update the value using the setter like this
onClick = {
updateItem(
item // Getter
.copy( //Convenience Method
quantity = item.quantity + 1 // Increase current Quantity
)
)
}
Just do the same for decrease and delete (updateItem(0)), and you should have accomplished this well... Finally. If you have any more doubts, just ask. I could assist over facetime if nothing works.
Based on #MARSK answer I've managed to achieve the goal (Thank you!)
Add a function to update the items value:
//Creating a function to update a certain item with a new value
fun updateShoppingItem(shoppingItem: ShoppingList, newValue: Int) {
val newInventoryMultiValue = shoppingItem.copy(currentInventory = shoppingItem.currentInventory.plus(newValue))
updateShoppingList(newInventoryMultiValue)
}
//Actually updating the room item with the function above
private fun updateShoppingList(shoppingItem: ShoppingList) {
viewModelScope.launch {
repository.updateShoppingItem(shoppingItem)
}
}
Then, in the Composable screen add the functions to the add and decrease buttons
val shoppingList by mainViewModel.getShoppingList.collectAsState(initial = emptyList())
LazyColumn() {
items(items = shoppingList)
{ item ->
InventoryCartScreenContents(
onAddClick = {
val newValue = 1
mainViewModel.updateShoppingItem(item, newValue)
},
onDecreaseClick = {
val newValue = -1
mainViewModel.updateShoppingItem(item, newValue)
},
}
)
}
}
}
Result

Passing values between classes in Vaadin

This might be more of a Java question, but how would you access values (say from a textfield) of a given view/class from a different class? For example if there was a TextField t1 that is in the MainView, and I wanted to get its current value for a computation in a different class. And is there a more Vaadin-specific approach here?
That can depend on the use case specifically. Since you mentioned a TextField value I assume the value is not yet stored in the DB, it's just on the UI yet -> I rule out singleton spring services.
A few ideas:
If the MainView and the different class are nested components and it's viable and not really complicated across a lot of classes... then probably passing it down the way when creating the sub-component. This is a naive solution - it can get pretty messy.
MainView() {
var t1 = new TextField();
var d = new Different(t1);
}
Fire and listen to Vaadin Component events. If you want really loose coupling, the most universal would be to use the UI instance as the event bus.
// listen in different class
ComponentUtil.addListener(attachEvent.getUI(), CloseMenuEvent.class, e -> closeMenu());
// fire change in MainView
ComponentUtil.fireEvent(ui, new CloseMenuEvent(ui))
A more specific version of number 2. is to pass the ValueChangeListener of the MainView's t1 to the different class.
MainView() {
var t1 = new TextField();
var d = new DifferentClass();
t1.addValueChangeListener(d::t1Changed)
add(t1, d);
}
Extract the common field to a third party, to a third class. Use a #UIScoped spring bean (#SpringComponent, #Service, ...) that will hold that field, and inject it to both MainView and the different class.
#Route
public class MainView extends VerticalLayout {
public MainView(Model m, Different d) {
add(m.t1, d);
}
}
#Scope(SCOPE_PROTOTYPE)
public class Different extends Component {
public Different(Model m) {
// something with m.t1
}
}
#UIScoped
public class Model {
public final TextField t1 = new TextField(); // TODO use getter
}
You could change the 4th approach by keeping String in Model and having a value change listener that updates it.

Generics types in VIPER subclassing. Cannot assign value of type 'ChildType' to type 'ParentType<Any>?'

I have VIPER architecture and I want to create parent VIPER search component that works with generic type. The idea is simple so I have search bar and I have table view. For example I can show drinks or foods names in table view. Depends on which generic data type I specified I want to show or foods or drinks.
I found very good example that solves my issue with generic view controllers. But I want to create something similar for swift generic VIPER architecture.
I will skip describing all VIPER classes like (Router, Interdictor and etc).
So I have parent view controller:
BaseSearchViewController: UIViewController {
var presenter: BaseSearchPresenter<Any>?
}
and child:
FoodSearchViewController: BaseSearchViewController {
}
This is a parent presenter specified in BaseSearchViewController
class BaseSearchPresenter<T> {
var items [T]
}
also I have child food search presenter with a specific type Food I would like to display in my table:
class FoodSearchPresenter: BaseSearchPresenter<Food> {
}
When I try to configure my VIPER:
let viewController = FoodSearchViewController(...) // init VC
let presenter = FoodSearchPresenter()
viewController.presenter = presenter // assigning child FoodSearchPresenter instance to a BaseSearchViewController presenter variable leads to this error:
Cannot assign value of type 'FoodSearchPresenter' to type 'BaseSearchPresenter<Any>?'
Here is a repo with the issue.
I left comments about how to attack this problem, but to your specific case, the answer is that BaseSearchViewController needs to be generic.
class BaseSearchPresenter<T> {
var items: [T] = []
}
// BaseSearchViewController must be generic in order to vary its Element
class BaseSearchViewController<Element>: UIViewController {
var presenter: BaseSearchPresenter<Element>?
}
struct Food {}
class FoodSearchPresenter: BaseSearchPresenter<Food> {}
class FoodSearchViewController: BaseSearchViewController<Food> {}
let viewController = FoodSearchViewController()
let presenter = FoodSearchPresenter()
viewController.presenter = presenter
To my point about starting concrete, I mean start with FoodSearchViewController as its own thing. Don't inherit from anything except UIViewController. Then build a second view controller that would make sense in this app and would need something similar. Then extract the commonality between them. This will tend to drive you in the right direction.

Ambiguous reference to member 'subscribe' Swift 3

I am new to Reactive programming, and I'm trying to observe a boolean value from my ViewModel in order to let my ViewController know when to start/stop the app's loader screen.
It's fairly simple and I want to use this method to avoid unnecessary delegates, since my ViewModel holds the business logic and my ViewController handles the UI.
My problem is this compiler error: Ambiguous reference to member 'subscribe'.
It also adds the two possible candidates, as you can see in the image below:
In my ViewModel, I've declared the observable as PublishSubject:
let done = PublishSubject<Bool>()
And I use it while observing another stream:
func subscribe() {
done.onNext(false)
anotherObservable.subscribe(
// other events observed here but not relevant to this matter
onCompleted: {
self.done.onNext(true)
}).addDisposableTo(rx_disposeBag)
}
And, finally, this is how I'm trying to handle it in the ViewController:
self.model.done.subscribe(
.onNext { isDone in
if isDone {
self.removeLoader()
}
}).addDisposableTo(rx_disposeBag)
I believe there is something simple I'm probably missing, so any help is appreciated.
In your second subscribe should be:
self.model.done.subscribe(onNext: { isDone in
if isDone {
self.removeLoader()
}
}).addDisposableTo(rx_disposeBag)

Resources