buttonsrtoys / mvvm_plus

State management package for Flutter that implements MVVM
MIT License
26 stars 2 forks source link

Accessing ViewWidget parameters from ViewModel #44

Open timbell opened 10 months ago

timbell commented 10 months ago

Currently I'm following this sort of pattern:

class MyViewWidget extends ViewWidget<MyViewModel> {
  MyViewWidget({
    super.key,
    this.param1,
    this.param2,
  }) : super(builder: () => MyViewModel());

  final int param1;
  final String param2;
...
  @override
  void didUpdateWidget(covariant MyViewWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // NB viewModel not available on "this" at this stage...
    // Forward on to view model...
    oldWidget.viewModel.didUpdateWidgetUpdate(this, oldWidget);
  }
...
}

class MyViewModel extends ViewModel {
...
  MyViewWidget get _widget => context.widget as MyViewWidget;

  void didUpdateWidget(MyViewWidget current, MyViewWidget old) {
    // eg resubscribe to streams dependent on widget parameters here...
    ...
  }

  String getSomething() {
    if (_widget.param1 > 10) {
      return 'red';
    }
    return _widget.param2;
  }
...
}

This works ok and more or less folllows what I think is the recommended way to do it vanilla Flutter.

So, my question: is this the intended way to access and handle changes to widget parameters from the view model?

buttonsrtoys commented 10 months ago

That would work, though I try to keep Flutter out of my ViewModels as much as possible. (An exception is I typically put Flutter controllers in ViewModels). So, my approach would be to pass the params to ViewModel, rather than the Widget. So,

class MyViewWidget extends ViewWidget<MyViewModel> {
  MyViewWidget({
    super.key,
    this.param1,
    this.param2,
  }) : super(builder: () => MyViewModel());

  final int param1;
  final String param2;
...
  @override
  void didUpdateWidget(covariant MyViewWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    viewModel.update(param1, param2);
  }
...
}

class MyViewModel extends ViewModel {
...
  void update(int param1, String param2) {
     // Update ViewModel here to accommodate param changes
  }
...
}

Apologies for the "viewModel not available" bug. I fixed and pushed this change just now, so if you upgrade to 1.5.3 you can use viewModel in didUpdateWidget.

timbell commented 10 months ago

Completely agree about keeping Flutter out of ViewModels! With the above you would need to pass the View parameters down in the ViewModel constructor as well to be able to handle the initial state. And this would imply duplicating the View parameters in the ViewModel unless I'm missing something?

What do you think about introducing a ViewParams abstraction? Something like this in mvvm+ itself:

abstract class ViewParams {}

abstract class ViewWidget<T extends ViewModel> extends StatefulWidget
    implements ViewParams {
...
}

class ViewState<T extends ViewModel> extends State<ViewWidget<T>>
    with BilocatorStateImpl<T> {
...
  T _buildViewModel() {
    ...
    viewModel.viewParams = widget;
    return viewModel;
  }

  @override
  void didUpdateWidget(covariant ViewWidget<T> oldWidget) {
    _viewModel.viewParams = widget;
    _viewModel.didUpdateViewParams(oldWidget);
    ...
  }
...
}

abstract class ViewModel<T extends ViewParams> extends Model {
...
  late T viewParams; // setable in user code?
...
  @protected
  @mustCallSuper
  void didUpdateViewParams(T oldViewParams) {}
}

Then in user code you could do:

abstract class MyViewParams implements ViewParams {
  int get param1;
  String get param2;
}

class MyViewWidget extends ViewWidget<MyViewModel>
    implements MyViewParams {
  MyViewWidget({
    super.key,
    this.param1,
    this.param2,
  }) : super(builder: () => MyViewModel());

  @override
  final int param1;
  @override
  final String param2;
 ...
}

class MyViewModel extends ViewModel<MyViewParams> {
  // View parameters are accessed through the viewParams member
...
  @override
  void didUpdateViewParams(MyViewParams oldViewParams) {
    // Update ViewModel here to accommodate ViewParams changes
  }
}

Use of ViewParams would be optional, existing code would work as before