Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
subzero911 opened this issue May 16, 2024 · 2 comments
Closed

Comments

@subzero911
Copy link
Contributor

subzero911 commented May 16, 2024

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
Copy link
Contributor Author

subzero911 commented May 21, 2024

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

User.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<User> users;

  Groups copyWith({
    ObservableList<User>? 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
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: <User>[].asObservable(),
      ),
      name: 'Max',
    );
  }

  @action
  void refreshUser() {
    var newUser = user?.copyWith(
      groups: Groups(
        users: [const User(name: 'John Doe')].asObservable(),
      ),
    );
    user = newUser;
  }
}
UI
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<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  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: <Widget>[
              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

image

@subzero911
Copy link
Contributor Author

subzero911 commented May 21, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant