우리는 StatelessWidget과 StatefulWidget을 이용하여 Flutter 어플리케이션을 개발합니다. 하지만, 어떠한 경우 앱의 상태관리를 위해서 Provider, GetX, BLoC 등을 이용하고 대부분은 StatelessWidget을 주로 이용하게 되죠.
하지만, 상태관리 솔루션을 적용하더라도 여러가지 경우 예를 들어, 애니메이션같은 경우 라이프사이클을 정의하기 위해서 StatefulWidget을 이용합니다.
StatefulWidget의 여러 메소드를 오버라이딩하여 상태를 정의하고 사용을 완료한 리소스를 반환하죠. 하지만, 실수 등을 통해 해당 로직이 정의되지 않으면 심한 경우 퍼포먼스에 영향을 끼칠 수 있고, 이러한 로직들은 반복적이기에 보일러 플레이트 코드를 생성하게 됩니다.
이를 해결하기 위해서 flutter_hooks가 등장했습니다.
flutter_hooks는 리엑트의 hook을 Flutter로 구현한 라이브러리입니다. hook은 불필요한 코드 중복을 제거함으로 코드 생산성을 증가시키는데 목적이 있다고 합니다.
중요한 것은 StatelessWidget과 StatefulWidget을 통합하여 HookWidget으로 사용한다는 것입니다.
flutter_hooks를 사용하여 얻을 수 있는 장점은 flutter_hooks 깃허브 레포의 간단한 예제를 통해 쉽게 알아볼 수 있습니다. 아래는 간단한 애니메이션을 선언한 StatefulWidget의 코드입니다. 이는 애니메이션 컨트롤러를 생성하고 Widget사용이 종료되면 dispose를 통해 controller를 해제합니다.
class Example extends StatefulWidget {
const Example({super.key, required this.duration});
final Duration duration;
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
}
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller.duration = widget.duration;
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container();
}
}
StatefulWidget을 이용하여 initState, didUpdateWidget, dispose 메소드를 정의하여 라이프사이클을 정의합니다. 이를 HookWidget에서는 어떻게 정의할 수 있을까요? 아래는 HookWidget으로 동일한 로직을 정의한 코드입니다.
class Example extends HookWidget {
const Example({super.key, required this.duration});
final Duration duration;
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
동일한 로직이지만, 엄청나게 코드가 줄어들었습니다. 어떻게 이를 가능하게 하는 것일까요?
바로 useAnimationController 메소드가 이 모든 로직을 담당하고 있기 때문입니다. 이 함수는 라이브러리에 모두 내장되어 있습니다. 그렇다면 어떻게 이러한 hook을 구현하였는지 알아보겠습니다.
기본적으로 State는 렌더링을 위하여 Element로 변환됩니다. 마찬가지로 hook도 Element로 변환됩니다. 하지만, State와 다르게
List<Hook>
에 저장됩니다. 그리고 사용을 위해서Hook.use
로 호출됩니다.use
를 통해서 반환되는 hook은use
의 호출횟수로 결정됩니다. 첫번째 호출은 첫번째 hook, 두번째 호출은 두번째 hook을 반환하는 것이죠. 이러한 과정은 구현 코드에서 직접 확인할 수 있습니다.
class HookElement extends Element {
List<HookState> _hooks;
int _hookIndex;
T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this);
performRebuild() {
_hookIndex = 0;
super.performRebuild();
}
}
위 코드는 hook을 담고있는 HookElement 정의코드입니다.
모든
hook
들은List<Hook>
에 저장되어 있기 때문에 생성 규칙을 갖고 있습니다.1. use로 시작하는 명명
Widget build(BuildContext context) {
// 올바른 예시.
useMyHook();
// 올바르지 못한 예시. 사용하는 사람들간 헷갈릴 수 있음.
myHook();
// ....
}
2. 조건문 없이 호출
Widget build(BuildContext context) {
useMyHook();
// ....
}
3. 조건문 안에서 호출하지 말기
Widget build(BuildContext context) {
if (condition) {
useMyHook();
}
// ....
}
flutter_hooks를 사용하면 코드 생산성이 많이 증가될 것 같네요. 또한, hooks에서 이미 제공하고 있는 기본적인 hook들이 있습니다. 이는 flutter_hooks의 README를 읽어보면 좋을 것 같습니다.