mobxjs / mobx.dart

MobX for the Dart language. Hassle-free, reactive state-management for your Dart and Flutter apps.
https://mobx.netlify.app
MIT License
2.39k stars 310 forks source link

@observable setter doesn't notify if inner ObservableList is changed #997

Closed subzero911 closed 3 months ago

subzero911 commented 3 months ago
image

Seems like it's not fixed for @observable members.

Store:

image

User and UserGroups class:

@JsonSerializable()
class User {
  const User({
    required this.id,
    required this.name,
    required this.role,
    required this.subrole,
    required this.phoneNumber,
    required this.hasMsisdn,
    this.picture,
    this.groups,
    this.accounts,
  });

  final String id;

  @JsonKey(defaultValue: '')
  final String name;

  @JsonKey(defaultValue: UserRole.unknown)
  final UserRole role;

  @JsonKey(defaultValue: UserSubrole.unknown)
  final UserSubrole subrole;

  @JsonKey(name: 'phone', defaultValue: 0)
  final int phoneNumber;

  @JsonKey(name: 'has_msisdn', defaultValue: true)
  final bool hasMsisdn;

  final String? picture;

  final List<UserGroups>? groups;

  final List<CoinAccount>? accounts;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  @override
  String toString() {
    return 'User(id: $id, name: $name, role: $role, subrole: $subrole, phoneNumber: $phoneNumber, hasMsisdn: $hasMsisdn, picture: $picture, groups: $groups, accounts: $accounts)';
  }

  User copyWith({
    String? id,
    String? name,
    UserRole? role,
    UserSubrole? subrole,
    int? phoneNumber,
    bool? hasMsisdn,
    String? picture,
    List<UserGroups>? groups,
    List<CoinAccount>? accounts,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      role: role ?? this.role,
      subrole: subrole ?? this.subrole,
      phoneNumber: phoneNumber ?? this.phoneNumber,
      hasMsisdn: hasMsisdn ?? this.hasMsisdn,
      picture: picture ?? this.picture,
      groups: groups ?? this.groups,
      accounts: accounts ?? this.accounts,
    );
  }

  @override
  bool operator ==(covariant User other) {
    if (identical(this, other)) return true;

    return other.id == id &&
        other.name == name &&
        other.role == role &&
        other.subrole == subrole &&
        other.phoneNumber == phoneNumber &&
        other.hasMsisdn == hasMsisdn &&
        other.picture == picture &&
        listEquals(other.groups, groups) &&
        listEquals(other.accounts, accounts);
  }

  @override
  int get hashCode {
    return id.hashCode ^
        name.hashCode ^
        role.hashCode ^
        subrole.hashCode ^
        phoneNumber.hashCode ^
        hasMsisdn.hashCode ^
        picture.hashCode ^
        groups.hashCode ^
        accounts.hashCode;
  }
}

@JsonSerializable()
class UserGroups {
  const UserGroups(this.id, this.code, {this.users});

  final String id;

  final String code;

  final ObservableList<User>? users;

  factory UserGroups.fromJson(Map<String, dynamic> json) => _$UserGroupsFromJson(json);

  @override
  String toString() => 'UserGroups(id: $id, code: $code, users: $users)';

  @override
  bool operator ==(covariant UserGroups other) {
    if (identical(this, other)) return true;

    return other.id == id && other.code == code && listEquals(other.users, users);
  }

  @override
  int get hashCode => id.hashCode ^ code.hashCode ^ users.hashCode;
}

Then I set a new User instance (with new users in group):

image

Observer doesn't react:

image

I also tried to check this behaviour and set it manually:

user = user.copyWith(...) -- Observer doesn't react

image

While directly changing ObservableList is working:

image
subzero911 commented 3 months ago

That's weird, I set up a minimal reproducible example, and everything works OK:

User.dart
```dart // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; class User { const User({ required this.name, this.groups, }); final String name; final Groups? groups; User copyWith({ String? name, Groups? groups, }) { return User( name: name ?? this.name, groups: groups ?? this.groups, ); } @override String toString() => 'User(name: $name, groups: $groups)'; @override bool operator ==(covariant User other) { if (identical(this, other)) return true; return other.groups == groups; } @override int get hashCode => Object.hash(name, groups);; } class Groups { const Groups({ required this.users, }); final ObservableList users; Groups copyWith({ ObservableList? users, }) { return Groups( users: users ?? this.users, ); } @override String toString() => 'Groups(users: $users)'; @override bool operator ==(covariant Groups other) { if (identical(this, other)) return true; return listEquals(other.users, users); } @override int get hashCode => users.hashCode; } ```
User Store
```dart import 'package:mobx/mobx.dart'; import '../models/user.dart'; part 'user_store.g.dart'; class UserStore = _UserStoreBase with _$UserStore; abstract class _UserStoreBase with Store { @observable User? user; @action void init() { user = User( groups: Groups( users: [].asObservable(), ), name: 'Max', ); } @action void refreshUser() { var newUser = user?.copyWith( groups: Groups( users: [const User(name: 'John Doe')].asObservable(), ), ); user = newUser; } } ```
UI
```dart import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx_inner_observablelist_bug/stores/user_store.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { late final UserStore userStore; @override void initState() { super.initState(); userStore = UserStore()..init(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text('MobX bug'), ), body: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Observer(builder: (_) { final userToString = userStore.user?.toString() ?? ''; return Text(userToString); }), const SizedBox(height: 24), Observer(builder: (_) { final familyUsers = userStore.user?.groups?.users; return Text(familyUsers.toString()); }) ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { userStore.refreshUser(); }, tooltip: 'Refresh', child: const Icon(Icons.refresh), ), ); } } ```
Results
![image](https://github.com/mobxjs/mobx.dart/assets/12999702/69be4040-66a7-4917-8658-765b2f1c850d) ![image](https://github.com/mobxjs/mobx.dart/assets/12999702/47d8df68-e209-4b28-be9e-322a0b068026)
subzero911 commented 3 months ago

I found a bug, and it's not MobX related. So, I saved a user into a final variable at the start of build() method, and it held a reference to the old user instance until the whole widget rebuilt image

After I started getting the user directly in the Observer, it began working image