mobx.dart icon indicating copy to clipboard operation
mobx.dart copied to clipboard

ObservableList is not detected by Observer

Open subzero911 opened this issue 3 years ago • 15 comments

I have the following code, where mapObjects is an ObservableList: image Output in the console: image

The declaration of ObservableList image

I don't need to wrap it into Observable<T>, as I never change the actual list reference, but the list content only: image I believe an Observer should be triggered and cause the rebuild, but it doesn't happen. Sorry for comments in Russian, just ignore them.

May be related to this discussion https://github.com/mobxjs/mobx.dart/discussions/765

subzero911 avatar Aug 12 '22 12:08 subzero911

I also tried to wrap it into Observable<ObservableList<MapObject>> and mutate the inner ObservableList, but Observer still does not rebuild (while it now sees the Observable inside). But, when I use Observable<List> (plain list) and change it like mapObjects.value = [clusterizedCollection], everything works fine.

subzero911 avatar Aug 12 '22 14:08 subzero911

I cannot figure out your full code ... but I think you need to annotate theObservableListwith @observable see this example from TODOs - lines 22 - 24 https://github.com/mobxjs/mobx.dart/blob/68998f0170f660e6f75bba15d9b424f138e9cc5e/mobx_examples/lib/todos/todo_list.dart#L24

giri-jeedigunta avatar Sep 08 '22 16:09 giri-jeedigunta

No, if I annotate it with @observable, it will mean that Observer now reacts to re-assigning the list reference, like todos = [...todos, newItem]; But I'm not going to do this. I want to mutate an existing list itself, like todos.add(newItem) (not creating a new list instance, but changing the existing list). The problem is that Observer does not detect this move for some reason.

Also, I don't use a code generation in my project and avoid any annotations. @observable ObservableList<Todo> todos; is just a pretty shortcut for Observable<ObservableList<Todo>> todos

subzero911 avatar Sep 08 '22 17:09 subzero911

Got it!

giri-jeedigunta avatar Sep 08 '22 17:09 giri-jeedigunta

Also, to recreate a list like todos = [...todos, newItem]; you don't even need ObservableList. It's enough to have a plain Dart List

@observable
List<Todo> todos;

The ObservableList power is to send notification on any change (add / remove / clear, etc.), so Observer will receive it. So I can change any inner collections in my model, without copyWith, and Observer will rebuild.

Moreover, it's one of the reasons why I ever use MobX. Because Flutter provides an "observable-like" solution out of the box, called ValueListenable. But there's nothing like ObservableList in Flutter.

subzero911 avatar Sep 08 '22 17:09 subzero911

May be add method of ObservableList is not implemented so it is not reactive, i.e. acting like plain add method. Need to check the source code of ObservableList.

deadsoul44 avatar Sep 08 '22 17:09 deadsoul44

It is implemented https://github.com/mobxjs/mobx.dart/blob/68998f0170f660e6f75bba15d9b424f138e9cc5e/mobx/lib/src/api/observable_collections/observable_list.dart#L104 so there should be a bug.

deadsoul44 avatar Sep 08 '22 17:09 deadsoul44

May be add method of ObservableList is not implemented so it is not reactive, i.e. acting like plain add method. Need to check the source code of ObservableList.

according to this topic https://github.com/mobxjs/mobx.dart/discussions/765 Observer does not notice the ObservableList only when it is passed to the parameter of the child widget. But it notices regular @observable or Observable<T> variables, passed to the child widget.

subzero911 avatar Sep 08 '22 17:09 subzero911

In that case, builder has a new context. There should be a new observer. In other words, there will be no reaction if there is not an observer in the immediate context.

deadsoul44 avatar Sep 08 '22 17:09 deadsoul44

But I cannot have an Observer inside of YandexMap, because it's a side-package. I cannot get into its source code and add an Observer.

Also, as I mentioned above, Observable<T> is perfectly tracked by Observer even if not in immediate context. So there should be a bug in Observer implementation, why it differently handles the Observable<T> and ObservableList

subzero911 avatar Sep 08 '22 17:09 subzero911

Can you provide the minimal reproducible code as a gist ? I don't think there is any bug in mobx here. There must be some missing implementation.

pr-1 avatar Sep 08 '22 19:09 pr-1

Hi @pr-1. I prepared a minimal app, showing the problem.

https://github.com/subzero911/mobx_observableList_bug_demo/tree/master/lib

If I directly use ListView inside of Observer, everything works fine. But if I pass ObservableList into child widget, which has a ListView inside, Observer does not track this ObservableList anymore.

But if I would use Observable<List> and re-assign a new list to it, Observer tracks the change as expected, despite I pass it into child widget too. I also added this variant into separate branch, so you can compare: https://github.com/subzero911/mobx_observableList_bug_demo/tree/observableT_variant/lib

subzero911 avatar Sep 09 '22 13:09 subzero911

Thanks for the code @subzero911 I have tried to explain what might be going wrong here.

Observer(builder: (_) {
              print('rebuilt');
              return SizedBox(
                width: 1024,
                height: 512,
                child: ChildWidget(
                  heartsList: controller.heartsList,
                ),
              );
            }),
class ChildWidget extends StatelessWidget {
  const ChildWidget({super.key, required this.heartsList});
  final ObservableList<String> heartsList;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: heartsList.length,
        itemBuilder: (context, index) {
          return SizedBox(
            width: 112,
            height: 48,
            child: ListTile(
              title: Text(heartsList[index]),
            ),
          );
        });
  }
}

The reactive context here will change and mobx will throw No observables found because Child widget creates its own new ObservableList variable which will have 3 hearts when it is built first time. This new ObservableList is used by the ListView. Please correct me if I am wrong here @pavanpodila

Instead make the child widget expect a List that is built from the ObservableList using controller.heartsList.toList() -

Observer(builder: (_) {
              print('rebuilt');
              return SizedBox(
                width: 1024,
                height: 512,
                child: ChildWidget(
                  heartsList: controller.heartsList.toList(),
                ),
              );
            }),
class ChildWidget extends StatelessWidget {
  const ChildWidget({super.key, required this.heartsList});
  final List<String> heartsList;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: heartsList.length,
        itemBuilder: (context, index) {
          return SizedBox(
            width: 112,
            height: 48,
            child: ListTile(
              title: Text(heartsList[index]),
            ),
          );
        });
  }
}

This is the reason why it is working as expected on the other branch . You can test it by making the List inside Child Widget a Observable like final Observable<List<String>> heartsList; on https://github.com/subzero911/mobx_observableList_bug_demo/tree/observableT_variant/lib This is equivalent to master branch code and it will no longer detect changes from controller.

Similarly for YandexMap try using this because YandexMap is not expecting an ObservableList but a List

Observer(builder: (_) {
              print('rebuilt');
              return SizedBox(
                width: 1024,
                height: 512,
                child: YandexMap(
                  mapObjects: _placesController.mapObjects.toList(),
                ),
              );
            }),

pr-1 avatar Sep 11 '22 06:09 pr-1

Thanks, that actually works! So the idea behind is that I passed the reference to original ObservableList to the child widget, and an Observer coudn't see any observables inside (because it's in a child's context, not in a current context). And it expects that I will create some Observer inside a ChildWidget and intercept the ObservableList. But if I pass a List copy to the child, an Observer now can see the original ObservableList modifying, in its context. So there's no error in MobX, but I used it wrong. Your code snippet should be in the ObservableList documentation, as it's not evident at all.

subzero911 avatar Sep 11 '22 15:09 subzero911

One of you should really add this to the docs... learned a bunch of things in this thread.

giri-jeedigunta avatar Sep 11 '22 18:09 giri-jeedigunta