[Flutter] Provider라이브러리 파헤치기 1 - InheritedWidget

S_Soo100·5일 전
0

flutter

목록 보기
20/20
post-thumbnail
  • Flutter를 처음 배우기 시작하면 보통 상태관리의 중요성과 프로바이더 라이브러리 부터 배웁니다.

  • 하지만 원리를 이해하고 싱글톤 패턴과 뭐가 다르구나 느껴보고 프로젝트에 잘 적용했다고 해도 그것이 어떻게 만들어졌는지 직접 열어본 사람은 적을 것 같습니다.

  • 더 깊은 이해를 위해 Provider 라이브러리의 lib 폴더를 분석하여 주요 구성 요소와 그 작동 원리를 상세하게 살펴보려 합니다.

🚀Provider pub dev 링크
🚀Provider github repo 링크
🚀InheritedWidget Flutter 공식문서 링크

뿌리를 찾아서, InheritedWidget

  • 프로바이더를 맨 처음 공부하며 이런 이미지를 많이 봤을 것입니다.
    위젯 트리에서 각자 멀리 떨어진 위젯들이 값을 공유하고, update되면 알려줄 수 있는 상태관리 기능이 프로바이더의 핵심이기 때문입니다.

  • 이런 WidgetTree 의 상위 위젯들에 접근하여 데이터를 가져오게 할 수 있도록 하는 근간이 바로 Inherited Widget입니다.

  • Provider 라이브러리의 다양한 Provider위젯들은 모두
    InheritedWidget를 베이스로 제작 되었습니다.
    그리고 InheritedWidget은 ProxyWidget을 상속하고 있는 추상 클래스인데, 몇몇 중요한 특징을 가지고 있습니다.

    1. 상태를 하위 위젯 트리에 전달
    2. BuildContext의 dependOnInheritedWidgetOfExactType() 메서드를 사용해 하위 위젯에서 해당 위젯을 찾고 상태를 구독
    3. updateShouldNotify를 오버라이드하여 상태가 변경될 때 하위 위젯들에게 알릴지 여부를 결정
  • 자동으로 화면을 리빌드하는 부분만 제외하면 Provider의 기본 기능과 동일합니다.

    InheritedWidget도 마찬가지로 위젯 트리의 상단에 배치해서 그 하위 위젯들에게 상태를 공유하는 역할을 하기 때문입니다.

    이제 Flutter를 열어 InheritedWidget의 코드를 보자.
    (보기 좋게 주석은 모두 제외하였음)

// flutter/lib/src/widgets/framework.dart
abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ super.key, required super.child });

  
  InheritedElement createElement() => InheritedElement(this);

  
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
  • override해 온 updateShouldNotify 메서드는 InheritedWidget이 가진 데이터가 변경되었을 때 그것이 기존의 데이터와 같은지 비교하고, 다를 경우에만 notify를 해줍니다.

    그리고 InheritedWidget의 실제 인스턴스이자 렌더링과 업데이트를 담당하는, InheritedElement를 만들어주는 createElement()메서드가 있습니다.

    그렇다면 부모를 확인했으니 우리도 Provider를 만들 수 있지 않을까요?

InheritedWidget으로 상태 관리하기

  • provider에 대해서 초보자들이 질문하면 가장 많이 받는 답변이 하나 있습니다.
    바로 "카운터 위젯을 Provider로 짜신 다음에 Flutter 기본 프로젝트랑 뭐가 다른지 비교해보세요!" 라는 말인데, (나 때는 그랬다)
    우리도 Provider 라이브러리를 열어 보기 전에 InheritedWidget을 가지고 똑같은 일을 해보겠습니다.

  • 우선 InheritedWidget으로 카운터를 만들겠습니다.

    숫자를 담는 count 변수와, count를 1씩 증가시키는 increment 함수만 가지고 있도록 단순하게 구성했다.
    그리고 InheritedWidget은 하나의 자식 Widget을 받는습니다.

// MyInheritedWidget.dart
class MyInheritedWidget extends InheritedWidget {
  final int count;
  final VoidCallback increment;

  MyInheritedWidget({
    Key? key,
    required this.count,
    required this.increment,
    required Widget child,
  }) : super(key: key, child: child);

  
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.count != count;
  }
}
  • 이제 이 MyInheritedWidget을 담은 Provider를 만듭니다.
// InheritedCounterProvider.dart
class InheritedCounterProvider extends StatefulWidget {
  final Widget child;

  const InheritedCounterProvider({Key? key, required this.child})
      : super(key: key);

  
  _InheritedCounterProviderState createState() => _InheritedCounterProviderState();
}

class _InheritedCounterProviderState extends State<InheritedCounterProvider> {
  int count = 0;

  void increment() {
    setState(() {
      count++;
    });
  }

  
  Widget build(BuildContext context) {
    return MyInheritedWidget(
      count: count,
      increment: increment,
      child: widget.child,
    );
  }
}
  • 자, 다 왔다. 이제 카운터를 보여줄 메인 페이지만 구성하면 됩니다.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return InheritedCounterProvider(
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          useMaterial3: false, 
        ), 
        home: InheritedHomePage(),
      ),
    );
  }
}

class InheritedHomePage extends StatelessWidget {
  const InheritedHomePage({super.key});

  
  Widget build(BuildContext context) {
    final counterWidget =
        context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();

    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter: ${counterWidget?.count ?? 0}',
                style: TextStyle(fontSize: 24)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: counterWidget?.increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

InheritedWidget 구현 분석

  • 이 구현의 포인트는 바로 BuildContext.dependOnInheritedWidgetOfExactType<T>() 메서드에 있으며, 이 메서드는 한 눈에 봐도 단어가 엄청 긴데, 'depend on inherited widget of "EXACT TYPE"', 즉, 상속된 위젯의 정확한 타입에 따라 달라진다. 라는 의미이며, 주석에는 이렇게 써있습니다.

    Returns the nearest widget of the given type T and creates a dependency on it, or null if no appropriate widget is found.

  • 즉, 위젯트리에서 T 타입의 위젯 중 가장 가까운 것을 반환 해줍니다.
    위의 코드에서는 MyInheritedWidget 타입을 찾아서 그 counter값과 increment함수를 사용했습니다.
    이것은 Provider에서 .of()메서드의 동작과 동일합니다.
    실제로 라이브러리를 열어보면 동일한 메서드를 사용하고 있습니다.
// provider/packages/provider/lib/src/provider.dart

...
static T of<T>(BuildContext context, {bool listen = true}) {
	...
    final inheritedElement = _inheritedElementOf<T>(context);
	...

    if (listen) {
      context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>();
    }
	...
    final value = inheritedElement?.value;
	...
    return value as T;
}
  • 하지만 Provider와는 다르게 InheritedCounterProvider 위젯 코드 내에서 setState()를 호출해서 화면을 리빌드 하고 있습니다.

    또한 Provider는 Multi Provider, Proxy Provider등 다양하고 중요한 기능들을 미리 개발해서 제공하고 있습니다.

  • 그러므로 위 코드는 단순히 Provider가 어떻게 만들어졌는지 파악하는 용도로 공부하면 좋을 것 같습니다.

    여기서 꼭 알아가야 할 포인트가 있다면 다음과 같습니다.

1. Provider의 부모인 InheritedWidget의 구성

  • Flutter에서 상태를 하위 위젯에 전달하는 기본적인 위젯
  • updateShouldNotify 메서드를 오버라이드하여 상태가 변경될 때 하위 위젯을 다시 빌드할지 결정
  • context.dependOnInheritedWidgetOfExactType<T>()를 사용해 하위 위젯에서 접근 가능

2. Widget Tree에서 InheritedWidget의 위치

  • 보통 앱의 최상단 또는 특정 상태를 공유해야 하는 범위에 배치
  • 그 하위 위젯들이 InheritedWidget의 데이터를 참조

3. InheritedWidget을 통한 상태관리

  • 상태를 전달만 하는 역할이므로 직접적인 상태 변경 기능은 없음
  • StatefulWidget과 조합하여 상태 변경이 필요할 때 setState와 함께 사용
  • Provider는 InheritedWidget을 상속하여 이 부분을 보강하였음!

다음 시간에는 본격적으로 Provider 라이브러리의 코드들을 열어보고 구성을 살펴보기로 하겠습니다.

profile
플러터, 리액트

0개의 댓글

관련 채용 정보