💻 개발/Flutter

[Flutter / Dart] What is Equatable?

고도고도 2022. 8. 6. 11:32

블로그 내의 게시물은 PC 버전에 최적화 되어 있습니다.

 

오늘은 Equatable에 대해 알아보려고 합니다. BLoC에 대한 글을 쓰기 전에 BLoC에서 사용하는 Equatable에 대한 정리가 우선적이라고 생각했습니다. 바로 본론으로 들어가보죠!

 

아래 코드를 먼저 살펴보겠습니다.

class Person {
  final int pid;
  final String name;
  final int age;

  Person(this.pid, this.name, this.age);
}


void main() {
  Person p1 = new Person(20221234, "kodo", 25);
  Person p2 = new Person(20221234, "kodo", 25);
  
 (p1 == p2) ? print("same") : print("diff");
}

Person 클래스를 정의하고 두 개의 인스턴스를 생성했습니다. 사원번호, 이름, 나이가 같은 두 개의 인스턴스를 생성하고 두 인스턴스를 비교했습니다. 인스턴스가 같다면 same을 출력하고 그렇지 않다면 diff를 출력합니다. 무엇이 출력될까요?

 

정답은 diff 입니다. 왜 그럴까요? 같은 멤버 변수를 가지지만 new 키워드를 통해 새로운 인스턴스를 생성했기에 서로 다른 주소를 참조하고 있습니다. 그렇다면 이런 상황에서 어떻게하면 same을 출력할 수 있을까요?

 

우선 간단하게 main에서 멤버 변수를 비교하는 방법이 있습니다.

class Person {
  final int pid;
  final String name;
  final int age;

  Person(this.pid, this.name, this.age);
}


void main() {
  Person p1 = new Person(20221234, "kodo", 25);
  Person p2 = new Person(20221234, "kodo", 25);
  
 (p1.pid == p2.pid) ? print("same") : print("diff");
}

하지만 pid를 직접 참조해야 하는 문제가 있습니다. 더 좋은 방법은 없을까요?

 

== 연산자를 오버라이딩하여 멤버 변수가 같으면 같은 인스턴스라는 것을 나타내면 됩니다.

class Person {
  final int pid;
  final String name;
  final int age;

  Person(this.pid, this.name, this.age);

  @override
  bool operator ==(Object other) {
    return other is Person && other.pid == this.age;
  }
}

void main() {
  Person p1 = new Person(20221234, "kodo", 25);
  Person p2 = new Person(20221234, "kodo", 25);

  (p1.pid == p2.pid) ? print("same") : print("diff");
}

끝났습니다! 여기까지 잘 따라오셨다면 Equatable은 완벽히 이해하신 겁니다. 👏👏 (벌써요?)

 

Equatable

서론이 길었지만 오늘 얘기할 Equatable은 인스턴스의 비교를 쉽게 할 수 있도록 도와줍니다. 우선 Equatable의 코드는 이렇게 생겼습니다.

@immutable
abstract class Equatable {
  /// {@macro equatable}
  const Equatable();

  /// {@template equatable_props}
  /// The list of properties that will be used to determine whether
  /// two instances are equal.
  /// {@endtemplate}
  List<Object?> get props;

  /// {@template equatable_stringify}
  /// If set to `true`, the [toString] method will be overridden to output
  /// this instance's [props].
  ///
  /// A global default value for [stringify] can be set using
  /// `EquatableConfig.stringify`.
  ///
  /// If this instance's [stringify] is set to null, the value of
  /// `EquatableConfig.stringify` will be used instead. This defaults to
  /// `false`.
  /// {@endtemplate}
  // ignore: avoid_returning_null
  bool? get stringify => null;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Equatable &&
          runtimeType == other.runtimeType &&
          equals(props, other.props);

  @override
  int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);

  @override
  String toString() {
    switch (stringify) {
      case true:
        return mapPropsToString(runtimeType, props);
      case false:
        return '$runtimeType';
      default:
        return EquatableConfig.stringify == true
            ? mapPropsToString(runtimeType, props)
            : '$runtimeType';
    }
  }
}

props를 비교하여 두 인스턴스가 같은지를 판별합니다. 또한 props로 hashcode를 생성합니다. hashcode의 경우 동일한 key로 중복 저장될 수 없는 Map과 Set에서 사용됩니다.

 

Equatable을 상속하는 Person 클래스를 정의하고 props의 getter를 오버라이딩하면 끝입니다!

import 'package:equatable/equatable.dart';

class Person extends Equatable {
  final int pid;
  final String name;
  final int age;

  Person(this.pid, this.name, this.age);
  
  @override
  List<Object?> get props => [this.pid];
}

void main() {
  Person p1 = new Person(20221234, "kodo", 25);
  Person p2 = new Person(20221234, "kodo", 25);

  (p1.pid == p2.pid) ? print("same") : print("diff");
}

 

그렇다면 왜 BLoC에 필요한거죠?

BLoC에서는 Event가 발생하면 State가 변경될 수 있도록 Mapping을 합니다. 이후 State를 감지하고 State에 맞는 작업을 진행합니다. 이때 각각의 Event와 State가 서로 다른 인스턴스라는 것을 알 수 있도록 Equatable을 상속받아 구현합니다.