본문 바로가기
Language & Framework/Dart & Flutter

[Flutter] ChangeNotifier 잘 쓰는 꿀팁!

by 6cess 2024. 1. 20.

Flutter 개발에서 상태 관리를 위해서 ChangeNotifier을 유연하고 강력하게 사용하는 것이 중요하다. 많은 Flutter 프로젝트의 아키텍쳐로 사용되는 MVVM 아키텍쳐에서는 Model / ModelView / VIew Layer 중 ModelView 계층을 ChangeNotifier 믹스인한 클래스로 표현할 만큼 Flutter 프로젝트에서 핵심적인 부분을 차지한다. 이렇게 자주 사용되는 ChangeNotifier을 잘 써먹을 수 있도록 기복적이지만 유용한 꿀팁들을 정리해본다!

1. 상태 캡슐화

  • 상태관리를 위해 외부에서 ChangeNotifier 믹스인한 클래스의 내부 상태 접근하는 경우가 빈번하게 발생한다. 이를 위해 접근 및 수정할 수 있는 getter와 setter를 작성하고 내부 상태를 private 변수로 선언한다. 이렇게 할 경우 조회하는 코드, 수정하는 코드를 구별하여 읽을 수 있는 가독성을 챙길 수 있을 뿐만 아니라 수정될 때 notifyListeners()를 함께 실행시켜 내부 상태 변경과 동시에 렌더링을 발생시킨다.
class CounterProvider with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

2. 상태 선택적 알림

  • 단순히 내부 상태 변수의 변경이 되었다고 해서 무조건 notifyListeners를 호출하는 것이 아니라 확실히 위젯 재빌드가 필요한 경우에만 notifyListeners를 호출하는지 고려하여 코드를 작성한다. 이렇게 반드시 필요한 경우에만 notifyListeners를 호출할 경우 렌더링을 최소화하여 성능 최적화를 기할 수 있다.
class UserProvider with ChangeNotifier {
  User _user;

  User get user => _user;

  void updateUser(User newUser) {
    if (_user != newUser) {
      _user = newUser;
      notifyListeners();
    }
  }
}

3. 비동기 작업 관리

  • 비동기 작업을 수행할 때, 작업의 상태를 보고 UI에 반영한다. 로딩 상태나 에러 등을 고려하여 notifyListeners을 요청한다. 예시에서는 비동기 요청이 오류가 발생하더라도 로딩 상태 렌더링이 진행되도록 작성한 코드이다.
class AsyncDataProvider with ChangeNotifier {
  Data _data;
  bool _isLoading = false;

  Data get data => _data;
  bool get isLoading => _isLoading;

  Future<void> fetchData() async {
    _isLoading = true;
    notifyListeners();

    try {
      _data = await fetchFromServer();
    } catch (error) {
      // 오류 처리
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

4. 의존성 분리

  • ChangeNotifier 믹스인을 통해 상태관리를 하다 보면 다른 클래스나 서비스에 접근해야 하는 경우가 많다. 즉 다른 말로 다른 클래스나 서비스에 의존하게 되는 경우가 있다. 이럴 경우 의존성 주입을 사용해서 의존성을 분리한다. 이를 통해 테스트하기 쉬운 코드, 결합도가 낮은 코드를 작성할 수 있다. MVVM 아키텍쳐에서는 ModelView Layer와 Model Layer의 분리를 위해 필수적인 작업이라고 할 수 있다.
class UserRepository {
  Future<User> fetchUser() async {
    // 사용자 데이터 가져오기
  }
}

class UserProvider with ChangeNotifier {
  final UserRepository _userRepository;

  UserProvider(this._userRepository);

  Future<void> loadUserData() async {
    // UserRepository 사용
    User user = _userRepository.getUserData();
  }
}

아래부터는 ChangeNotifier을 확장해서 자주 사용되는 방법이다

Dispose 상태 확인하기

  • ChangeNotifier믹스인을 통해 notifyListeners을 호출할 때 간혹 위젯이 이미 dispose되어 에러가 발생하는 경우가 있다. 이를 방지하는 간단한 믹스인이다.
mixin SafeNotifyMixin on ChangeNotifier {
  bool _disposed = false;

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  void safeNotifyListeners() {
    if (!_disposed) {
      notifyListeners();
    }
  }
}

Throttling

  • 너무 잦은 notifyListeners 호출을 방지하여 상태가 반영되는 속도는 줄이되 리소스 절약을 하고 싶다면 Throttle 도입을 고려해보자.
mixin ThrottlingMixin on ChangeNotifier {
  DateTime _lastCallTime;
  Duration throttleDuration = Duration(milliseconds: 500);

  @override
  void notifyListeners() {
    final now = DateTime.now();
    if (_lastCallTime == null || now.difference(_lastCallTime) > throttleDuration) {
      _lastCallTime = now;
      super.notifyListeners();
    }
  }
}

로그 찍기

  • 렌더링 시 로그를 남기는 믹스인을 사용해보자. 디버그 과정에서 상태 변경을 추적하는 데 유용할 수 있다
mixin LoggingMixin on ChangeNotifier {
  @override
  void notifyListeners() {
    print('Listeners notified in $this at ${DateTime.now()}');
    super.notifyListeners();
  }
}

상태 관리 마스터가 되는 날까지...