InheritedWidget과 riverpod의 provider

ds-k.dev·2025년 1월 8일
0

Flutter 깊게 보기

목록 보기
2/4

InheritedWidget이란?

  • InheritedWidget은 Flutter에서 위젯 트리의 하위 위젯들에게 데이터를 효율적으로 전달하기 위해 사용되는 특별한 위젯.
  • 일반적으로 상태를 공유해야 하는 위젯 트리의 상위에 위치하며, 하위 위젯들이 InheritedWidget을 통해 상태를 구독하고 변경 사항을 감지할 수 있다.

InheritedWidget의 특징

  1. 데이터 전달: 위젯 트리의 하위 위젯들이 InheritedWidget을 통해 데이터를 접근할 수 있다.
  2. 효율성: 상태가 변경될 때, 변경된 부분만 다시 빌드되므로 성능이 최적화됩니다.
  3. 구독: 하위 위젯들은 InheritedWidget의 상태를 구독하고, 상태가 변경될 때 자동으로 업데이트됩니다.

예시 코드

import 'package:flutter/material.dart';

class CounterInheritedWidget extends InheritedWidget {
  const CounterInheritedWidget({
    super.key,
    required this.counter,
    required this.incrementCounter,
    required super.child,
  });

  final int counter;
  final VoidCallback incrementCounter;

  static CounterInheritedWidget of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
    assert(result != null, 'CounterInheritedWidget를 찾을 수 없습니다');
    return result!;
  }

  
  bool updateShouldNotify(CounterInheritedWidget oldWidget) {
    return oldWidget.counter != counter;
  }
}
  • inheritedWidget을 extends 하는 class 생성
  • 관리할 상태와 변경 함수를 받음(선택 사항)
  • 하지만 반드시 child는 포함해야 함.

of (BuildContext context) 메소드

InheritedWidget의 인스턴스를 찾아 반환하는 정적(static) 메소드

  static CounterInheritedWidget of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
    assert(result != null, 'CounterInheritedWidget를 찾을 수 없습니다');
    return result!;
  }
  1. 위젯 트리 탐색: BuildContext를 통해 위젯 트리를 위로 탐색하며 가장 가까운 InheritedWidget을 찾음
  2. 의존성 등록: dependOnInheritedWidgetOfExactType를 사용하여 현재 위젯을 InheritedWidget(여기서는 상위 CounterInheritedWidget)의 종속성으로 등록 (구독 개념)
  3. 자동 재빌드: 등록된 위젯은 InheritedWidget의 상태가 변경될 때 자동으로 리빌드.

updateShouldNotify(oldWidget) 메소드

값이 다른 경우에만 true를 반환하여 불필요한 리빌드를 방지하는 메소드


  bool updateShouldNotify(CounterInheritedWidget oldWidget) {
    return oldWidget.counter != counter;
  }

이전 위젯의 count 변수와 현재 counter 변수를 비교해서 변경이 될때만 리빌드하게 하는 메소드

사용법

상태를 내려주고 싶은 상단 위젯에 CounterInheritedWiget으로 감싸준다.

  • 상단 위젯 : 상태 및 상태 변경 함수 정의
class _InheritedCounterPageState extends State<InheritedCounterPage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return CounterInheritedWidget(
      counter: _counter,
      incrementCounter: _incrementCounter,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Inherited Widget 카운터'),
        ),
        body: const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Inherited Widget으로 구현한 카운터:'),
              InheritedCounterDisplay(),
              InheritedCounterButton(),
            ],
          ),
        ),
      ),
    );
  }
}
  • 하위 위젯 : 구독자
class InheritedCounterDisplay extends StatelessWidget {
  const InheritedCounterDisplay({super.key});

  
  Widget build(BuildContext context) {
    final counter = CounterInheritedWidget.of(context).counter;
    return Text(
      '$counter',
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

Riverpod의 Provider과의 차이점

  • 상태 선언 방식:
    InheritedWidget: 위젯 트리 내부에 선언
    Riverpod: 위젯 트리와 독립적으로 전역에서 선언 가능
  • 타입 안정성:
    InheritedWidget: 런타임에 위젯 존재 확인
    Riverpod: 컴파일 타임에 타입 체크 가능
  • 상태 관리 로직:
    InheritedWidget: 위젯과 함께 구현
    Riverpod: 별도의 Provider 클래스로 분리 가능
  • 의존성 주입:
    InheritedWidget: BuildContext를 통한 위젯 트리 탐색
    Riverpod: ProviderContainer를 통한 직접 참조

즉 Inherited Widget의 단점을 보완하고 전역적으로 상태를 쓰고 읽을 수 있는 기능을 제공하는 것

어떻게 그게 가능한가?

결국 inheritedWidget의 특성을 이용한다!

// 1. 앱 최상위에 ProviderScope 배치
void main() {
  runApp(
    ProviderScope(  // 여기서 ProviderContainer 생성
      child: MyApp(),
    ),
  );
}

// 2. ProviderScope의 내부 구현 (단순화된 버전)
class ProviderScope extends StatefulWidget {
  
  State<ProviderScope> createState() => _ProviderScopeState();
}

class _ProviderScopeState extends State<ProviderScope> {
  // ProviderContainer 인스턴스 생성
  late final container = ProviderContainer();

  
  Widget build(BuildContext context) {
    // UncontrolledProviderScope는 InheritedWidget을 상속
    return UncontrolledProviderScope(
      container: container,
      child: widget.child,
    );
  }
}

// 3. ConsumerWidget에서 접근
class MyWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // ref는 내부적으로 BuildContext를 사용하여
    // 가장 가까운 UncontrolledProviderScope를 찾아
    // 그 안의 ProviderContainer에 접근
    final value = ref.watch(someProvider);
    return Text('$value');
  }
}

UncontrolledProviderScope

// UncontrolledProviderScope는 InheritedWidget을 상속받음
class UncontrolledProviderScope extends InheritedWidget {
  const UncontrolledProviderScope({
    required this.container,
    required Widget child,
    Key? key,
  }) : super(key: key, child: child);

  // ProviderContainer 인스턴스를 보관
  final ProviderContainer container;

  // InheritedWidget의 필수 구현 메서드
  
  bool updateShouldNotify(UncontrolledProviderScope oldWidget) {
    return container != oldWidget.container;
  }

  // of 메서드를 통해 ProviderContainer에 접근
  static ProviderContainer containerOf(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>();
    return scope!.container;
  }
}

즉 결국은 inheritedWidget의 기능을 추상화해서 만든 라이브러리라고 볼 수 있다.

예제

https://github.com/ds-k/inherited_widget_sample

  • inheritedWidget과 Provider를 통해 구현한 카운터 예제를 확인하실 수 있습니다.

0개의 댓글

관련 채용 정보