Listenable이란? Animation과 ChangeNotifier를 통해서 알아보자.
포스트
취소

Listenable이란? Animation과 ChangeNotifier를 통해서 알아보자.

AnimationController와 SingleTickerProviderStateMixin

개발을 하다 보니 1프레임마다 widget을 업데이트 해 주어야 하는 상황이 생겼습니다.

보통 이럴 때에는 1프레임을 정확하게 받기 위해서(사용자 기기의 성능도 다르고 구현 상황에 따라 프레임이 다르기 때문에)

StatefulWidget에서 SingleTickerProviderStateMixin을 mix 해 주고, mix한 클래스에서 this로 tick 정보를 받아서 구현하곤 합니다.

this는 AnimationController를 초기화할 때 vsync로 넣어주면 되지요.

정리하면 이렇게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AWidget extends StatefulWidget{
  ...
}

class _AWidgetState extends State<AWidget> with SingleTickerProviderStateMixin{
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
    	duration: const Duration(milliseconds: 1000),
      vsync: this,
    )
  }
}

이렇게 초기화 한 AnimationController를 이용해서 우리는 AnimatedBuilder 안에 animation값을 넣고 프레임마다 animation을 연산 하여 프레임마다 바뀌는 구현을 해 줍니다.

1
2
3
4
5
6
AnimatedBuilder(
  animation: _animation!,
  builder: (context, child) => CustomPaint(
    painter: LinePainter(_animation!.value),
  ),
)

그러면, 실제 AnimatedBuilder의 구현은 어떻게 되어있길래, animation 값이 바뀔때마다 내부의 위젯을 업데이트 해 줄 수 있는걸까요?

StatefulWidget과 구현이 다르다면, 이렇게 tick마다 업데이트 되는 위젯에 한해서 setState를 해 주는 것보다 더 효율적인 방법이 flutter에 존재하는 것 일까요?

AnimatedBuilder

구현은 다음과 같이 되어있었습니다. (주석 삭제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AnimatedBuilder extends ListenableBuilder {
  const AnimatedBuilder({
    super.key,
    required Listenable animation,
    required super.builder,
    super.child,
  }) : super(listenable: animation);

  Listenable get animation => super.listenable;
  
  @override
  Listenable get listenable => super.listenable;

  @override
  TransitionBuilder get builder => super.builder;
}

AnimatedBuilderListenableBuilder 의 구현에 매우 의존하고 있다는 것을 알았습니다.

ListenableBuilder를 알아 볼 차례네요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ListenableBuilder extends AnimatedWidget {
  const ListenableBuilder({
    super.key,
    required super.listenable,
    required this.builder,
    this.child,
  });

  @override
  Listenable get listenable => super.listenable;
  final TransitionBuilder builder;
  final Widget? child;

  @override
  Widget build(BuildContext context) => builder(context, child);
}

AnimatedWidget에 대해서도 알아봐야겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    super.key,
    required this.listenable,
  });

  final Listenable listenable;

  @protected
  Widget build(BuildContext context);
  @override
  State<AnimatedWidget> createState() => _AnimatedState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Listenable>('listenable', listenable));
  }
}

AnimatedWidget은 abstract class ! 였습니다.

StatefulWidget이었고, 사실상 ListenableBuilder가 implementation을 하고 있는데, build 메소드가 우리가 인자로 주고있는 builder를 그냥 return하는것 뿐이네요.

우리가 들었던 예시로 보면

1
2
3
4
5
6
AnimatedBuilder(
  animation: _animation!,
  builder: (context, child) => CustomPaint(
    painter: LinePainter(_animation!.value),
  ),
)

여기서의 builder를 그대로 build하는것으로 보입니다.

StatefulWidget의 구현에 별 로직이 없었으므로, AnimatedWidget의 state에 대해서도 알아봐야겠네요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    if (!mounted) {
      return;
    }
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

이렇게 구현이 되어있었습니다.

정리를 하면, initState일 때 listenableaddListener를 합니다.

_handleChange는 말 그대로.. widget이 mount되어있다면 setState만을 해 주고 있네요.

나머지코드는 listenable이 바뀌었다면 removeListeneraddListener를 해 주고 있는것이고, 위젯이 dispose될 때 removeListener를 해 주고 있습니다.

그렇다면 Listenable이라는것은 무엇이길래, 값 변화를 감지하고 addListener되어있는 함수들은 호출하는 지 궁금해졌습니다.

Listenable

가장 먼저 listenable을 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
abstract class Listenable {
  const Listenable();

  factory Listenable.merge(Iterable<Listenable?> listenables) = _MergingListenable;

  void addListener(VoidCallback listener);

  void removeListener(VoidCallback listener);
}

이와 같이 아주 간단하게 abstract class인 것을 볼 수 있습니다.

listener를 더하고 뺄 수 있는 함수가 있습니다.

가장 대표적으로 Listenable를 implements하는 클래스는 ChangeNotifier라고 할 수 있습니다.

ChangeNotifier

ChangeNotifier는 change notification을 제공해줄 수 있는 mixin class입니다.

구현을 확인해보면 300줄에 달할정도로 긴 구현임을 확인할 수 있습니다.

핵심적인부분만 글에 담아보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
mixin class ChangeNotifier implements Listenable {
  int _count = 0;

  static final List<VoidCallback?> _emptyListeners = List<VoidCallback?>.filled(0, null);
  List<VoidCallback?> _listeners = _emptyListeners;
  int _notificationCallStackDepth = 0;
  int _reentrantlyRemovedListeners = 0;
  bool _debugDisposed = false;

	...

  @override
  void addListener(VoidCallback listener) {
	...
    if (_count == _listeners.length) {
      if (_count == 0) {
        _listeners = List<VoidCallback?>.filled(1, null);
      } else {
        final List<VoidCallback?> newListeners =
            List<VoidCallback?>.filled(_listeners.length * 2, null);
        for (int i = 0; i < _count; i++) {
          newListeners[i] = _listeners[i];
        }
        _listeners = newListeners;
      }
    }
    _listeners[_count++] = listener;
  }

...

  @override
  void removeListener(VoidCallback listener) {
    for (int i = 0; i < _count; i++) {
      final VoidCallback? listenerAtIndex = _listeners[i];
      if (listenerAtIndex == listener) {
        if (_notificationCallStackDepth > 0) {
          _listeners[i] = null;
          _reentrantlyRemovedListeners++;
        } else {
          _removeAt(i);
        }
        break;
      }
    }
  }


  @protected
  @visibleForTesting
  @pragma('vm:notify-debugger-on-exception')
  void notifyListeners() {
    assert(ChangeNotifier.debugAssertNotDisposed(this));
    if (_count == 0) {
      return;
    }

    _notificationCallStackDepth++;

    final int end = _count;
    for (int i = 0; i < end; i++) {
      try {
        _listeners[i]?.call();
      } catch (exception, stack) {
       ...
      }
    }

    _notificationCallStackDepth--;

		...
  }
}

가장 핵심적인 함수는 addListenerremoveListener, notifyListeners입니다.

abstract였던 함수들이 concrete하게 구현되는 부분들이죠.

notifyListeners를 보면 _listener들을 하나한 call해주는 것을 볼 수 있습니다.

실제로 ChangeNotifier를 extends해서 쓰는 ValueNotifier는 내부의 값이 변할 때 그 값이 예전 값과 같지 않다면 notifyListeners를 호출합니다. 이 함수들을 들여다보면 listener들을 직접 call해주는걸 확인할 수 있었습니다.

listener들은 하나의 리스트로 관리되고 addListener와 removeListener가 그 리스트에 listener들을 추가하고 제거해주는 역할을 해 주는 것이죠.

Animation

animation은 다소 특이합니다.

처음엔 ChangeNotifier와 비슷할것이라고 생각했는데, 아무리 내부 구현을 확인해도 실제 listener를 호출하는 코드는 확인할 수 없었습니다.

정답은, Animation은 listener들에게 notify할 때 AnimationControllerAnimationLocalListenerMixin의 구현에서 notify한다는 것 이었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mixin AnimationLocalListenersMixin on Animation<double> {
  List<VoidCallback>? _listeners = [];

  @protected
  void notifyListeners() {
    if (_listeners != null) {
      final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners!);
      for (final VoidCallback listener in localListeners) {
        try {
          listener();
        } catch (exception, stack) {
          FlutterError.reportError(FlutterErrorDetails(
            exception: exception,
            stack: stack,
            library: 'animation library',
            context: ErrorDescription('while notifying listeners for $runtimeType'),
            informationCollector: () => <DiagnosticsNode>[
              DiagnosticsProperty<Animation<double>>(
                'The $runtimeType notifying listeners was',
                this,
                style: DiagnosticsTreeStyle.errorProperty,
              ),
            ],
          ));
        }
      }
    }
  }

  @override
  void addListener(VoidCallback listener) {
    _listeners!.add(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    _listeners!.remove(listener);
  }
}

실제 구현은 이렇게 되어있었죠. 아까 봤던 ChangeNotifier의 구현은 addListener와 removeListener에 좀 더 공을 쓴 느낌이었는데 이건 그냥 간단하게 구현이 되어 있네요.

마찬가지로 listener를 호출하는 방식으로 listen을 구현하고 있습니다. 이번엔 call을 호출하는게 아니라 그냥 함수를 실행하는 방식이네요.

Listenable한 건 Animation인데 어째서 AnimationController에서 notify의 실구현이 되어있고 이게 어떻게 작동하는건가요? 라고 묻는다면, AnimationControllerAnimation을 상속하고 있다는 사실을 말씀드리고 싶습니다.

CurvedAnimation, AlwaysStoppedAnimation… 등도 Animation을 상속하고 있고, AnimationControllerAnimation을 상속하고 있고… AnimationListenable을 상속하고있고.. 결국 모든게 Listenable하지만, 실제로 Listenable한게 유효한 대상은 AnimationController라고 볼 수 있겠습니다만.. 사실은 CurvedAnimation이든 특정 작동하는 Animation을 초기화할때 파라미터인 parent 값에 AnimationController를 넣어줘야 합니다. 그리고 이 작동하는 Animation들은 AnimationWithParentMixin을 mix하고있고, 이 mixin에서는 작동하는 Animation에 추가되는 addListener를 parent에 addListener하도록 hooking해주고 있죠.

결국 구현은 AnimationController에 되어 있지만 실제 사용할때에는 Animation들 모두가 AnimationController의 listener구현에 따라 작동한다는 것이고, 그 실제 구현은 AnimationLocalListenersMixin에 되어있다는게 결론입니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

ios 수출규정 관련 문서 누락 경고 해결

Flutter 개발환경 설정