본문 바로가기
비전공자를 위한 Flutter/Flutter 심화과정

Flutter InheritedWidget 란? 코드 뜯어보기 (주석 한글 번역)

by 밍잔 2022. 5. 5.

지난 시간에 StatefulWidget을 통해 상태를 관리하는 것까지 알아봤는데요. 성능을 위해 StatelessWidget을 더 잘게 쪼개서 사용해야 한다는 것도 아셨죠? 그렇다면 불가피하게 위젯 트리의 계층이 깊어질 수 밖에 없는데요. 이때 상위에 있는 State 정보를 매개변수로 자식 위젯으로 계속해서 전달하는 일은 산 꼭대기에서 출발지점까지 한 명씩 줄지어서 돌을 전달하는 것과 같죠. 만약 100명 중 50번째 쯤에서 잘못 서 있던 바람에 돌을 다시 전달해야 한다면...? 생각만 해도 어지럽습니다.

이걸 100번 해야 한다면 진짜 할 겁니까 휴먼?

 


 


이런 귀찮고 비효율적인 일을 해결해줄 InheritedWidget이 있습니다! 이 위젯의 자식 위젯들은 해당 위젯의 데이터에 바로 접근할 수 있는 기능이 있습니다. 산 꼭대기에서 출발지점까지 돌을 그냥 던져주는 겁니다. 아래 그림처럼 말이죠!


내 밑으로 하나씩 하나씩 다 전달...
한 번에 전달!



아래는 2018년에 만든 구글에서 InheritedWidget에 대해 소개하는 영상입니다. 자동자막을 설정하면 자막을 보실 수 있어요!

 

[자동자막 > 한글]로 설정하세요!



빌드 메소드의 context에 있는 BuildContext.dependOnInheritedWidgetOfExactType. 프로퍼티를 통해 가장 가까운 InheritedWidget의 데이터에 접근할 수 있습니다. 이렇게 참조된 위젯은 state 정보가 변하는 경우 StatefulWidget과 같이 다시 빌드하게 됩니다.


컨벤션은 InheritedWidget의 BuildContext.dependOnInheritedWidgetOfExactType. 를 요청하는 static of 메소드를 제공하는 겁니다. 이렇게 하면 클래스가 해당 클래스만의 로직을 정의할 수 있게 합니다. 위의 예시에서는 null이 리턴 되는데, 기본값을 가질 수도 있습니다. 경우에 따라 of 메소드는 위젯이 아니라 데이터를 리턴합니다. 예제의 경우 FrogColor 위젯 말고 Color 데이터만 리턴해도 되는 거죠. 우연히 InheritedWidget이 다른 클래스에서 구현될 수도 있으니, private이어야 합니다.

 

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key? key,
    required this.color,
    required Widget child,
  }) : super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    final FrogColor? result = context.dependOnInheritedWidgetOfExactType<FrogColor>();
    assert(result != null, 'No FrogColor found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}


다음과 같은 경우에는 of 메소드는 일반적으로 퍼블릭 클래스를 씁니다. 예를 들어 Theme는 상속된 비공개 위젯을 빌드하는 StatelessWidget으로 구현됩니다. Theme.of는 상속받은 위젯들이 BuildContext.dependOnInheritedWidgetOfExactType를 사용하여 ThemeData를 리턴받은 것과 같습니다. 


class MyPage extends StatelessWidget {
  const MyPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FrogColor(
        color: Colors.green,
        child: Builder(
          builder: (BuildContext innerContext) {
            return Text(
              'Hello Frog',
              style: TextStyle(color: FrogColor.of(innerContext).color),
            );
          },
        ),
      ),
    );
  }
}


이렇게 .of(context)를 사용할 수 있죠. Theme.of(context)로 이미 익숙하신가요? Theme은 사실 InheritedWidget의 한 예시였던 겁니다. Scaffold, FocusScope도 마찬가지죠. InheritedWidget의 데이터 필드는 변경할 수 없습니다. 참조만 할 수 있죠.

 


여러 계층 아래로 데이터를 전달해야할 때 대처법을 알아봤습니다. 도움이 되셨나요?

다음 강의에서는 Key에 대해 알아봅시다. Key를 제대로 사용하면 재빌드 대신 재사용을 할 수 있으니 퍼포먼스 향상을 기대할 수 있습니다. 아래 링크를 눌러주세요!


https://mingzan.tistory.com/267

 


아래는 관련 내용에 대한 번역입니다. 오역이 있다면 댓글로 남겨주세요!

 

/// 새로운 위젯을 빌드하는 대신 자식 위젯으로 제공되는 위젯
///
/// [InheritedWidget]과 [ParentDataWidget]처럼 유용한 위젯들입니다.
///
///  * [InheritedWidget]는 하위 위젯들로부터 state가 읽어지는 위젯입니다.
///  * [ParentDataWidget], for widgets that populate the
///    [RenderObject.parentData] slot of their child's [RenderObject] to
///    configure the parent widget's layout.

///  * [ParentDataWidget]은 [RenderObject]의 사이즈를 부모의 레이아웃으로 설정하기 위해
///    [RenderObject.parentData] 프로퍼티에 채워지는 위젯입니다.

///  * [StatefulWidget]과 [State]는 시간이나 생명주기에 따라 다르게 빌드하기 위한 위젯입니다.
///  * [StatelessWidget]은 항상 주어진 정보로만 빌드되어 있는 위젯입니다.
abstract class ProxyWidget extends Widget {
  /// 하나의 자식 위젯만 갖는 위젯을 생성합니다.
  const ProxyWidget({Key? key, required this.child}) : super(key: key);

  /// 이 위젯 아래에 트리 안에 있는 위젯이 생성됩니다.
  ///
  /// 이 위젯은 하나의 자식 위젯만 가질 수 있습니다.
  /// 여러 자식 위젯들을 레이아웃 시키기 위해서는 이 위젯을 [Row], [Column], [Stack] 등
  /// children 프로퍼티를 가진 위젯을 사용해야 합니다.
  final Widget child;
}

/// [RenderObjectWidget]들의 자식 위젯들이 [ParentData] 정보에 접근할 수 있는 기본 클래스 입니다.
///
/// 이것은 하나 이상의 자식이 있는 [RenderObjectWidget]에 대한 자식별 구성을 제공하는 데 사용할 수 있습니다.
/// 예를 들어, [Stack]은 [Positioned] 상위 데이터 위젯을 사용하여 각 하위를 배치합니다.


/// [ParentDataWidget]은 특정한 종류의 [ParentData]의 일부입니다. 
/// 그런 매개변수 클래스 유형을 `T`라고 부릅니다.
///
/// 이 예제는 어떻게 [ParentDataWidget]을 이용해 각각의 자식 위젯들에게 사이즈를 설정하는지 보여줍니다.
///
/// ```dart
/// class FrogSize extends ParentDataWidget<FrogJarParentData> {
///   const FrogSize({
///     Key? key,
///     required this.size,
///     required Widget child,
///   }) : super(key: key, child: child);
///
///   final Size size;
///
///   @override
///   void applyParentData(RenderObject renderObject) {
///     final FrogJarParentData parentData = renderObject.parentData! as FrogJarParentData;
///     if (parentData.size != size) {
///       parentData.size = size;
///       final RenderFrogJar targetParent = renderObject.parent! as RenderFrogJar;
///       targetParent.markNeedsLayout();
///     }
///   }
///
///   @override
///   Type get debugTypicalAncestorWidgetClass => FrogJar;
/// }
/// ```
/// {@end-tool}
///
/// 참고해보세요:
///
///  * [RenderObject] : 레이아웃 알고리즘의 슈퍼클래스 입니다.
///  * [RenderObject.parentData] : 이 클래스의 설정 슬롯입니다.
///  * [ParentData] : [RenderObject.parentData] 슬롯에 입력되는 데이터의 슈퍼클래스입니다.
///    [ParentDataWidget]를 위한 `T` 타입 파라미터가 [ParentData]입니다.
///  * [RenderObjectWidget] : [RenderObject]들을 감싸는 위젯입니다.
///  * [StatefulWidget] 와 [State] : 위젯을 생명주기와 시간에 따라 다르게 빌드할 수 있습니다.
abstract class ParentDataWidget<T extends ParentData> extends ProxyWidget {
  /// 추상 const 생성자.
  /// 이 생성자는 서브클래스에게 const 생성자를 사용할 수 있게 합니다.
  const ParentDataWidget({Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  ParentDataElement<T> createElement() => ParentDataElement<T>(this);

  /// 이 `renderObject`에 주어진 부모 데이터를 위젯에 그대로 적용할 수 있는지 체크합니다. 
  ///
  /// `renderObject`에 주어진 [RenderObject.parentData]는 [RenderObjectWidget] 조상에 의해 
  /// [debugTypicalAncestorWidgetClass] 클래스로 설정됩니다.
  ///
  /// 이 메소드는 [applyParentData]를 실행하기 바로 전에 호출됩니다.
  bool debugIsValidRenderObject(RenderObject renderObject) {
    assert(T != dynamic);
    assert(T != ParentData);
    return renderObject.parentData is T;
  }

  /// 일반적으로 [applyParentData]가 쓸 [ParentData]를 설정하는 데 사용되는 [RenderObjectWidget]입니다.
  ///
  /// 어떤 위젯을 이 ParentDataWidget위젯으로 감쌌는지 에러메시지를 보여줄 때만 호출됩니다.
  Type get debugTypicalAncestorWidgetClass;

  Iterable<DiagnosticsNode> _debugDescribeIncorrectParentDataType({
    required ParentData? parentData,
    RenderObjectWidget? parentDataCreator,
    DiagnosticsNode? ownershipChain,
  }) sync* {
    assert(T != dynamic);
    assert(T != ParentData);
    assert(debugTypicalAncestorWidgetClass != null);

    final String description =
        'The ParentDataWidget $this wants to apply ParentData of type $T to a RenderObject';
    if (parentData == null) {
      yield ErrorDescription(
        '$description, which has not been set up to receive any ParentData.',
      );
    } else {
      yield ErrorDescription(
        '$description, which has been set up to accept ParentData of incompatible type ${parentData.runtimeType}.',
      );
    }
    yield ErrorHint(
      'Usually, this means that the $runtimeType widget has the wrong ancestor RenderObjectWidget. '
      'Typically, $runtimeType widgets are placed directly inside $debugTypicalAncestorWidgetClass widgets.',
    );
    if (parentDataCreator != null) {
      yield ErrorHint(
        'The offending $runtimeType is currently placed inside a ${parentDataCreator.runtimeType} widget.',
      );
    }
    if (ownershipChain != null) {
      yield ErrorDescription(
        'The ownership chain for the RenderObject that received the incompatible parent data was:\n  $ownershipChain',
      );
    }
  }

  /// 이 위젯으로부터의 데이터를 주어진 렌더 오브젝트들의 부모 데이터로 작성합니다.
  ///
  /// 프레임워크는 [RenderObject]와 관련된 자식 위젯의 [RenderObject.parentData]가
  /// 오래 됐을 때 언제든 감지하고 이 메소드를 호출합니다.
  /// 예를 들어, 렌더 오브젝트가 렌더 트리에 하나 삽입되는 경우, 
  /// 렌더 오브젝트의 부모 데이터는 이 위젯의 데이터와 일치하지 않을 가능성이 있습니다.
  ///
  /// 서브 클래스들은 주어진 렌더 오브젝트의 데이터를 자신의 [RenderObject.parentData]필드로 복사하기 위해
  /// 이 함수를 오버라이드해야 합니다. 
  ///
  /// 렌더 오브젝트들의 부모는 `T`유형의 위젯에 의해 생성되었다는게 보증됩니다.
  /// 이는 일반적으로 이 함수가 렌더 오브젝트의 부모 데이터 객체가 
  /// 특정 클래스에서 상속된다고 가정할 수 있음을 의미합니다.
  /// 
  /// 이 [applyParentData]함수가 부모의 레이아웃이나 페인팅을 변경할 수 있는 데이터를 수정하는 경우 
  /// 이 함수는 부모의 [RenderObject.markNeedsLayout] 또는 [RenderObject.markNeedsPaint] 호출을 담당합니다.
  @protected
  void applyParentData(RenderObject renderObject);

  /// [ParentDataElement.applyWidgetOutOfTurn] 메소드가 허용됩니다.
  ///
  /// 만약 위젯이 레이아웃 페이즈에서 아무런 변경 없이 [ParentData]의 설정을 그대로 나타낸다면 true를 리턴합니다. 
  @protected
  bool debugCanApplyOutOfTurn() => false;
}

/// 효과적으로 데이터를 트리 아래로 전달하기 위한 기본 클래스 입니다.
///
/// [BuildContext.dependOnInheritedWidgetOfExactType]를 이용해 
/// 가장 가까운 InheritedWidget 인스턴스를 얻을 수 있습니다.
///
/// InheritedWidget이 참조되면 해당 위젯의 state가 변할 때 리빌드를 요청합니다.
///
/// {@tool snippet}
///
/// The following is a skeleton of an inherited widget called `FrogColor`:
///
/// ```dart
/// class FrogColor extends InheritedWidget {
///   const FrogColor({
///     Key? key,
///     required this.color,
///     required Widget child,
///   }) : super(key: key, child: child);
///
///   final Color color;
///
///   static FrogColor of(BuildContext context) {
///     final FrogColor? result = context.dependOnInheritedWidgetOfExactType<FrogColor>();
///     assert(result != null, 'No FrogColor found in context');
///     return result!;
///   }
///
///   @override
///   bool updateShouldNotify(FrogColor old) => color != old.color;
/// }
/// ```
/// {@end-tool}
///
/// ## `of` 메소드 구현
///
/// 규칙은 [BuildContext.dependOnInheritedWidgetOfExactType]에 대한 호출을 수행하는
/// [InheritedWidget]에 'of' 정적 메서드를 제공하는 것입니다. 
/// 이렇게 하면 범위에 위젯이 없는 경우 클래스가 자체 폴백 논리를 정의할 수 있습니다. 
/// 위의 예에서 반환된 값은 이 경우 null이 되지만 기본값이 값일 수도 있습니다.
///
/// 다음과 같은 경우에는 of 메소드는 일반적으로 퍼블릭 클래스를 씁니다. 
/// 예를 들어 Theme는 상속된 비공개 위젯을 빌드하는 StatelessWidget으로 구현됩니다. 
/// Theme.of는 상속받은 위젯들이 BuildContext.dependOnInheritedWidgetOfExactType를 
/// 사용하여 ThemeData를 리턴받은 것과 같습니다. 
/// 
/// ## `of` 메소드 호출
///
/// 'of' 메서드를 사용할 때 'context'는 [InheritedWidget]의 하위 항목이어야 합니다. 
/// 즉, 트리의 [InheritedWidget] "아래"에 있어야 합니다.
///
/// {@tool snippet}
///
/// 이 예제에서, `context`를 [Builder]로 사용됐습니다. 
///
/// ```dart
/// class MyPage extends StatelessWidget {
///   const MyPage({Key? key}) : super(key: key);
///
///   @override
///   Widget build(BuildContext context) {
///     return Scaffold(
///       body: FrogColor(
///         color: Colors.green,
///         child: Builder(
///           builder: (BuildContext innerContext) {
///             return Text(
///               'Hello Frog',
///               style: TextStyle(color: FrogColor.of(innerContext).color),
///             );
///           },
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
///
/// {@tool snippet}
///
/// 이렇게 .of(context)의 형태로도 사용할 수 있죠.
///
/// ```dart
/// class MyOtherPage extends StatelessWidget {
///   const MyOtherPage({Key? key}) : super(key: key);
///
///   @override
///   Widget build(BuildContext context) {
///     return Scaffold(
///       body: FrogColor(
///         color: Colors.green,
///         child: Text(
///           'Hello Frog',
///           style: TextStyle(color: FrogColor.of(context).color),
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
///  * [StatefulWidget]과 [State]는 시간이나 생명주기에 따라 다르게 빌드하기 위한 위젯입니다.
///  * [StatelessWidget]은 항상 주어진 정보로만 빌드되어 있는 위젯입니다.
///  * [Widget], 위젯에 대한 개요입니다.
///  * [InheritedNotifier], 값이 [Listenable]이 될 수 있고 
///    값이 알림을 보낼 때마다 종속 항목에 알릴 상속된 위젯일 수도 있습니다.
///  * [InheritedModel], 클라이언트가 값의 하위 부분에 대한 변경 사항을 
///    구독할 수 있도록 하는 상속된 위젯입니다.
abstract class InheritedWidget extends ProxyWidget {
  /// 추상 const 생성자.
  /// 이 생성자는 서브클래스에게 const 생성자를 사용할 수 있게 합니다.
  const InheritedWidget({Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  /// 프레임워크는 위젯에게 이 위젯으로부터 상속했는지를 알려야 합니다.
  ///
  /// 이 위젯이 리빌드 되면, 상속받은 위젯도 리빌드를 해야 하지만, 가끔 그러지 않을 때도 있습니다.
  /// 예를 들면, oldWidget에 있던 데이터와 동일한 데이터를 가진 경우, 리빌드할 필요가 없습니다.
  ///
  /// 프레임워크는 위젯들의 트리 위치를 미리 파악해놓음으로써 이런 케이스들을 구별합니다.
  /// 이 위젯들은 같은 runtimeType 객체로 보증됩니다.
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

 

댓글