Stacked-Org / stacked

A Flutter application architecture created from real world scenarios
MIT License
972 stars 255 forks source link

[stacked] Updating View Models from a parent #62

Closed creativecreatorormaybenot closed 4 years ago

creativecreatorormaybenot commented 4 years ago

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:

// A widget contains a ViewModelBuilder
viewModelBuilder: () => AppleViewModel(apples)

Now, the AppleViewModel will be initalized with 10. However, the AppleWidget might change and have 11 apples at a later point. Unfortunately, the view model needs to act differently with 11 apples. However, the viewModelBuilder 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 with stacked. In a StatefulWidget, I have access to didChangeDependencies, which allows me to solve the above problem using Provider/InheritedWidget or I can use didUpdateWidget if the value is passed as a parameter.

FilledStacks commented 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

creativecreatorormaybenot commented 4 years ago

@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 ViewModels will not automatically be recreated. Instead, I have built a workaround like this:

Stateful view model ```dart class StatefulViewModel extends StatefulWidget { const StatefulViewModel({Key key, @required this.data}) : assert(data != null), super(key: key); final Data data; @override State createState() => _StatefulViewModelState(); } class _StatefulViewModelState extends State { DataViewModel _viewModel; @override void initState() { super.initState(); _viewModel = DataViewModel(widget.data); } @override void didUpdateWidget(StatefulViewModel oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.data != widget.data) { _viewModel.data widget.data; } } @override Widget build(BuildContext context) { return ViewModelBuilder.nonReactive( viewModelBuilder: () => _viewModel, builder: (context, viewModel, child) => const DataViewModelWidget(), ); } } ``` I am not really satisfied with this example (it is not exactly like the real use case), but it should illustrate the point with the above explanation.
FilledStacks commented 4 years ago

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:

  • The app in the restaurant for Delivery Dudes
  • Customer facing app (busy with that) for Delivery Dudes
  • Drivers app coming up next for Delivery Dudes
  • a memory sharing app for a startup
  • An anonymous reporting app for The Guardian to report abuse withing businesses and school systems.
  • A payment "system" to make lay-buy purchases easier
  • An automation tool
  • V2 of AppSkeletons.com for web and ipad with on device preview of what you're building.
  • A incident tracking tool for workers in the Oil fields.

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.

creativecreatorormaybenot commented 4 years ago

@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?

FilledStacks commented 4 years ago

@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.

essboyer commented 4 years ago

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.

FilledStacks commented 4 years ago

@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.

elhe26 commented 4 years ago

@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 ?

essboyer commented 4 years ago

@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!

FilledStacks commented 4 years ago

@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.

FilledStacks commented 4 years ago

@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.

elhe26 commented 4 years ago

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.

FilledStacks commented 4 years ago

@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.

elhe26 commented 4 years ago

Thanks. I'm looking forward to that implementation.

zkx2020 commented 4 years ago

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 .For simple example,I have a class Title which extends changenotifier,and I have a List title inside that class.I add notifierlistener() so when the list title value change I can update three different state which use the same value. It's seem that I should use the global viewmodel but one view has its own viewmodel..The three state are in three different view.

FilledStacks commented 4 years ago

@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 commented 4 years ago

@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!

jahir9991 commented 10 months ago

so using key: ValueKey( {dynamic value} ), may solve the problem, but not sure if is it the correct approach.