Closed creativecreatorormaybenot closed 4 years ago
Hi there,
The problem with recreating a viewmodel in that way is that it's not removed from the widget tree so the previous one isn't automatically disposed. We currently have a createModelOnInsert property which will fire the viewmodel builder every time the widget is inserted into the tree. I haven't seen this type of scenario yet and I've built quite a few apps.
Could you give some pseudo code to explain when and why this would happen. Maybe I can show you the way we do this on our side. If we can't it might be an opportunity for me to improve the package :)
-Dane
@FilledStacks I have read that you have built a few apps with it - is there some list of what kind of apps that is?
I will try to illustrate one of the cases:
--- StreamViewModel (listens to some database for a list of data from a service)
|
--- ViewModelWidget (builds a list for the data)
|
--- ViewModel (created for each child in the list of data)
|
--- ViewModelWidget (represents a data entry)
When the list of data is updated and the StreamViewModel
at the top of tree updates, the underlying ViewModel
s will not automatically be recreated. Instead, I have built a workaround like this:
Aaaah, I see. That's almost considering the ViewModel as an extension of the UI and forcing it to adopt the same top down approach. That data those "child" viewmodels need I would update in a service and make use of between all the viewmodels.
i don't have a portfolio but I've built we've quite a few apps. The latest being a food delivery service app for Delivery Dudes. I'll list some of the apps here:
Your approach makes sense. I wont' call it a ViewModel as it can't be unit tested (flutter has to be started up for it to be tested). ViewModels should always be unit testable. But that can definitely be built into the ViewModelBuilder. You can do that and make a PR and I'll merge it in.
Add a property to the constructors (optional), rebuildWithNewData
which is false by default. If that's true then we can do the didUpdateWidget check you have using the viewmodel for comparison and then forcing the ViewModelBuilder to fire again.
In addition to that you have to most likely dispose the previous viewmodel and then assign the new one incase a user has overridden the dispose function in their viewmodel. That should work right? This will be the first feature added that I don't use and that's not coming from me so you'll have to advise on if that would work and make a PR for it. I'd be happy to merge it in.
@FilledStacks I see that I would want to store the data in a service, however, I am not sure how to do that.
If I have the list of data I talked about and need to perform operations on it (like loading additional data for each list element), how would I build a list in a widget that reacts to the changes in the list of data? Would I just use one view model?
@creativecreatorormaybenot This all sounds very normal to me but it depends on the situation. An example would probably be the best way to describe it so I can see what the problem is in the form of a code example.
You could use one ViewModel and use the ViewModelWidget to access that data in the child widgets. that's what i do. Most of my widgets that are view specific do that.
This might possibly be the solution for an issue I am having, as well. Or perhaps I'm just missing something.
I have an app which uses the ViewModelBuilder to construct the body of a Scaffold. In it, is a form, with many different types of fields. The fields will update the ViewModel correctly, but, if something in the model changes (like say I call an API to populate a field), the new value isn't updated in the UI, even when calling notifyListeners(), until the UI builds again (ie. I nav away and then back in).
For a specific example, I have a text field in which a user can enter a URL that points to an audio file. They can also upload an audio file, which will return the URL for the uploaded file, and update the model with the new URL. The updated URL value will not be shown in the text field until the page is rebuilt.
@essboyer If you're using that value in your view and you call notifyListeners then it'll call the builder again where you have the chance to rebuild your UI with the latest value. You're either not using the correct ViewModel (maybe another instance was created somehow) or your builder is not firing again. Print out the value of your property at the beginning of the builder to see what it is.
@essboyer, I had a similar issue (Bottom-Up approach) when I was implementing a dark mode switch button.
Stacked's guidelines:
ViewModels for widgets that represent page views are bound to a single View only. ViewModels may be re-used if the UI required the exact same functionality. ViewModels should not know about other ViewModels.
The thing is that each page/widget is bound to its view model. This means that every time you call notifyListeners, you'll see changes in that page.
I bent these rules a bit to get darkMode:
ViewModels for widgets that represent page views are bound to a single View only. Disregarding this rule, you can have the following:
app.dart
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<MyAppViewModel>.reactive(
builder: (context, model, child) => MaterialApp(
// Routing, navigator keys, etc... omitted
theme: AppTheme.themeData(context, isDarkTheme: model.isDarkTheme),
),
viewModelBuilder: () => MyAppViewModel(),
onModelReady: (model) => model.initialize(),
);
}
}
I create sort of a "global" view model to change global ThemeData. Now, using MyAppViewModel with Home View:
class HomeView extends ViewModelWidget<MyAppViewModel> {
const HomeView({Key key}) : super(key: key);
@override
Widget build(BuildContext context, MyAppViewModel model) {
return Scaffold(
appBar: HomeBar(),
backgroundColor: Theme.of(context).backgroundColor,
);
}
}
Using ViewModelWidget with HomeBar widget:
class HomeBar extends ViewModelWidget<MyAppViewModel>{
HomeBar({Key key}) : super(key: key);
@override
Widget build(BuildContext context, MyAppViewModel model) {
return AppBar(
// Logic omitted...
actions: <Widget>[
IconButton(
icon: Icon(
!model.isDarkTheme ? Icons.wb_sunny : FontAwesomeIcons.moon,
color: Theme.of(context).appBarTheme.actionsIconTheme.color,
),
onPressed: () {
if (model.isDarkTheme) {
model.setDarkTheme(value: false);
} else {
model.setDarkTheme(value: true);
}
},
),
],
);
}
This configuration gives me the result I was expecting. Since MyAppViewModel is used to basically switch to dark mode, I added the HomeViewModel functionality to MyAppViewModel. This approach practically follows this rule:
ViewModels may be re-used if the UI required the exact same functionality.
What do you think of that @FilledStacks ?
@FilledStacks Thanks for taking the time to address my issues. I'm a HUGE fan of your work and videos, and I really appreciate it!
I've created a little demo application to demonstrate my issue: https://github.com/essboyer/mvvm_test
The app has an instance of my implementation of a TextInputFormField
. The init
method on my view model starts a timer which changes the view model's data model object with a random text value every 2 seconds. If you set breakpoints on the model, you can see the value change. If you set breakpoints in the TextInputFormField
and see the updated value being passed in, yet the UI does not update to show the new value. Perhaps the issue is with my implementation of the TextInputFormField
and I'm just not seeing it.
Thanks again!
@elhe26 What you're trying to do there is give ViewModels the job of services. Service or theme manager can store all of that. A viewmodel can be made reactive and when the value changes the viewmodels update. Themes are also global so it can be provided once and used everywhere in your app.
You definitely don't need a global ViewModel. You can even do it like this.
@essboyer TextFormField is stateful internally so that won't work. It's not expected to work. you change a text field value by updating its controller. The initialValue is only used when first inserted into the tree as you can see here from the source code.
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
} else {
widget.controller.addListener(_handleControllerChanged);
}
}
After that the value is only updated when the controller is changed. This problem also has nothing to do with updating viewmodels from a parent? I thought there would be more than 2 viewmodels involved. a parent and a child.
Either way, that's not a stacked problem. Even useing basic setState won't work since text fields are stateful internally. constructing a new one doesn't update its state. That's why they're stateful.
Thanks @FilledStacks . How would I implement the theme manager using stacked? I want to switch from dark mode to light mode using a switch. Since this will be changing MaterialApp's theme, I came up with a pseudo-global viewmodel to modify the theme. I implemented the viewmodel for MaterialApp (MyAppViewModel) to trigger the theme change. Moving the trigger to homeviewmodel will not work.
@elhe26 the same is in theme manager but you'll control the theme manager from a theme service. The same way we did with the original dialog manager / service pair where the manager interacts with the ui and the service sends commands to the UI from the viewmodels.
It does add a bit of overhead in code. 1 extra class and a rework. In this case i wouldn't actually mind having a global viewmodel. I personally wouldn't do it but that restriction is also just there because I "think" it'll cause side effects. If there are none I think you can stick to it.
I have something similar where I have this OverlayView which surrounds the child in the builder function, which is the entire app. This allows me to show overlay widgets at any time and it has it's own viewmodel so in a sense I'm also doing that. Stick with it for now, once I have a clear example of why it fails i will share it. If it makes it easier for you the do it that way.
Thanks. I'm looking forward to that implementation.
I am new in flutter,I use stacked it's helpful but I have a little question..I want to update three different state at the same time,but I don't know how to do that. Before using stacked I used ChangeNotifierprovider and wrap three different state in the same Consumer
@zkx2020 I show that in this video here . We use a service to communicate between different viewmodels which can then react to changes in that service.
@zkx2020 I show that in this video here . We use a service to communicate between different viewmodels which can then react to changes in that service.
Your hard work help a lot of people,Thank you!
so using key: ValueKey( {dynamic value} ), may solve the problem, but not sure if is it the correct approach.
I am not sure if this should be a feature request as I might be misunderstanding the proper usage of the
stacked
package in this scenario.Essentially: what do I do if I need a view model that reacts to the widget tree.
Say, I have the following:
Now, the
AppleViewModel
will be initalized with10
. However, theAppleWidget
might change and have11
apples at a later point. Unfortunately, the view model needs to act differently with11
apples. However, theviewModelBuilder
will not be called again.How do I update a view model based on a value that changed in the widget tree?
To add a little bit of context, the
apples
value would also be controlled by a view model. Thus, I basically want to update a view model from a parent view model.If this is not yet implemented and I am not missing how to properly use the architecture, this would be a feature request 🙂 (let me know if my train of thought here is incorrect altogether)
StatefulWidget
Usually, this task would be accomplished using a
StatefulWidget
and to my understanding, I should not use them withstacked
. In aStatefulWidget
, I have access todidChangeDependencies
, which allows me to solve the above problem usingProvider
/InheritedWidget
or I can usedidUpdateWidget
if the value is passed as a parameter.