ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] 플러터 종속성 주입 (InheritedWidgets/ get_it / provider)
    웹 개발/Flutter 2021. 1. 28. 19:02
    반응형

     

    개발을 하다보면 들어갈 데이터는 다르지만 UI가 같은 객체를 서로 다른 클래스에 넣어야할 때가 있다. 

    클래스마다 데이터만 다른 같은 객체를 일일히 추가하다보면 코드가 길어지고

    수정사항이 생길 때는 객체별로 코드를 수정해야하기 때문에 매우 번거로워 진다. 

     

    이러한 불편함을 해결하기 위해 Dependency Injection (종속성 주입) 기술이 필요하다.

    DI는 코드의 재사용성을 높이는 데 사용되는 기술이며 해당 객체를 클래스마다 인스턴스화 즉 생성하는 대신 

    클래스에 종속 객체를 주입한다. 

     

    클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화라고 하며, 어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스라고 한다. 결국 인스턴스는 객체와 같은 의미이지만, 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖고 있으며, 인스턴스는 어떤 클래스로부터 만들어진 것인지를 강조하는 보다 구체적인 의미를 갖고 있다. <자바의 정석 중>

     

    flutter web의 페이지 네비게이션에서는 DI 개념이 중요하기 때문에 DI 기술들을 알아보았다. 

    www.filledstacks.com/post/flutter-basics-going-from-set-state-to-architecture/


     

     

     

    1) 생성자(constructor)를 이용한 파라미터(parameter) 전달

     

    일반적으로 우리가 가장 먼저 배우는 방법이다.

    부모 클래스는 자식 클래스인 HomeView class에 AppInfo 객체의 파라미터(parameter)를 직접 전달하고

    전달받은 파라미터는 생성자(constructor)를 통해

    HomeView class에 주입되어 HomeView는 AppInfo 객체에 의존하게 된다.

    class HomeView extends StatelessWidget {
      AppInfo appInfo;
      HomeView({Key key, this.appInfo}) : super(key: key); //생성자
      @override
      Widget build(BuildContext context) {
        return Container(
        );
      }
    }

     

    간단하지만 파라미터를 전달해야할 위젯 트리의 깊이가 깊어지거나 전달해야할 파라미터의 수가 많다면 

    불필요한 코드들이 증가하기 때문에 다른 방법을 찾아야한다. 

     

     

     

    2) Inherited Widget

     

    state관리를 위해 flutter 패키지에 포함된 위젯이다.

    상위 특정 위젯이 하위 위젯과 함께 데이터를 공유하기 위해서 사용되며 

    상태관리를 하고자하는 하위 위젯들을 Inherited Widget으로 감싼다. 

     

     

    특징

     

    - Inherited Widget으로 감싼 하위위젯들만 상태관리가 가능하기 때문에 일방향적인 데이터 흐름을 강제한다.

    - 하위 위젯에서는 데이터 읽기만 가능하고 수정은 상위 위젯에 접근해야만 가능하다.

    - context를 사용할 수 없는 위젯에서는 거의 불가능하다.

    - of()를 호출할 때마다 해당 메소드가 있는 build context가 재빌드 되기 때문에 성능저하가 발생 할 수 있다.

     

     

     

    사용 예시

     

    - 전체 코드

    class DIExercisePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: MyStatefulWidget(
            child: MyContainer(
              child: DummyContainer(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    CounterLabel(),
                    CounterValue(),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    

     

    - 상위 위젯

    class MyStatefulWidget extends StatefulWidget {
      final Widget child;
    
      const MyStatefulWidget({Key key, @required this.child}) : super(key: key);
    
      static MyStatefulWidgetState of(BuildContext context) {
        return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>().data;
      }
    
      @override
      MyStatefulWidgetState createState() {
        return MyStatefulWidgetState();
      }
    }
    
    class MyStatefulWidgetState extends State<MyStatefulWidget> {
      int _counterValue = 0;
    
      int get counterValue => _counterValue;
    
      void addCounterBy1() {
        setState(() {
          _counterValue += 1;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return MyInheritedWidget(
          child: widget.child,
          data: this,
        );
      }
    }

    상위 위젯인 MyStatefulWidget에서는 _counterValue를 parameter로 전달하지 않고 

    하위 위젯을 MyInheritedWidget로 감싼다. 

    하위 위젯트리들은 MyInheritedWidget을 통해 상위 위젯의 데이터에 접근할 수 있다. 

     

    dependOnInheritedWidgetOfExactType는 하위 위젯 widget.child가 buildcontext에 쌓인
    상위 위젯인 MyInheritedWidget 인스턴스에 접근할 수 있도록 한다. 

     

     

    - InheritedWidget

    class MyInheritedWidget extends InheritedWidget {
      final MyStatefulWidgetState data;
    
      MyInheritedWidget({
        Key key,
        @required Widget child,
        @required this.data,
      }) : super(key: key, child: child);
    
      @override
      bool updateShouldNotify(InheritedWidget oldWidget) {
        return true;
      }
    }
    

     

    updateShouldNotify는 프레임워크가 해당 위젯(oldWidget)이 수정사항이 있을 때

    상속받는 위젯들에게 알려 재 빌드되어야 할지 여부를 지정한다. 

     

    해당 위젯이 보유하는 데이터가 이전 위젯이 보유하는 데이터와 동일하면

    이전 위젯이 보유하는 데이터를 상속받은 위젯을 재구성할 필요가 없다.

     

     

    - 데이터 수정 

    class MyContainer extends StatelessWidget {
      final Widget child;
    
      MyContainer({
        Key key,
        @required this.child,
      })  : super(key: key);
    
      void onPressed(BuildContext context) {
        MyStatefulWidget.of(context).addCounterBy1();
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Container(
            width: 200,
            height: 200,
            child: RaisedButton(
              color: Colors.red,
              onPressed: (){
                onPressed(context);
              },
              child: child,
            ),
          ),
        );
      }
    }

     

    MyStatefulWidget.of(context).addCounterBy1();

     

    하위 위젯은 static method인 of 를 이용하여 context를 전달하고

    상위 위젯인 MyStatefulWidget의 addCounterBy1 함수에 접근하여 데이터를 수정한다.

     

     

    - 하위 데이터에서 데이터 읽기

    class CounterValue extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return CounterValueState();
      }
    }
    
    class CounterValueState extends State<CounterValue> {
      int counterValue;
      double fontSize;
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        MyStatefulWidgetState data = MyStatefulWidget.of(context);
        counterValue = data.counterValue;
        fontSize = 50.0 + counterValue;
      }
    
      @override
      Widget build(BuildContext context) {
        return Text(
          "$counterValue",
          style: TextStyle(
            fontSize: fontSize,
            color: Colors.white,
          ),
        );
      }
    }

     

    MyStatefulWidgetState data = MyStatefulWidget.of(context);

    counterValue = data.counterValue;

     

    아래의 코드도 마찬가지로 of를 사용하여 상위 위젯인 MyStatefulWidget의 counterValue에 접근하여 

    _CounterValue 데이터를 사용했다. 

     

    아래의 링크에 가면 더 자세한 원리를 알 수 있다.
    medium.com/manabie/how-does-flutter-inheritedwidget-work-3123f9d74c15
     

    How does Flutter InheritedWidget work?

    Pass data from an ancestor widget to descendant widgets, which are possibly deep down the widget tree.

    medium.com

     

     

     

    3) get_it

     

    get it package는 간단한 service locator이다.

     

    특징

     

    - global locator를 사용하여 어디에서든 type을 요청가능하다.

    - 위젯을 포장하거나 context가 없어도 사용가능하다. 

    - 인스턴트 추적(instance tracking)은 Factories or Singleton를 통해 자동으로 처리된다. 

    - flutter의 프로모션과 달리 다방향 데이터 흐름인 전역 객체 사용한다. 

     

     

    사용예시

     

    - 우선 get it package를 pubspec에 추가한다. 

    dependencies:
         get_it: ^5.0.6

     

    - service_locator.dart

    import 'package:get_it/get_it.dart';
    
    GetIt locator = GetIt.instance;
    
    void setupLocator() {
      locator.registerFactory(() => AppInfo());
    }

    locator.dart 안에 GetIt의 instance를 저장하고 해당 파일을 import한 어디에서든 전역적으로 접근가능하도록 한다. 

    setupLocator라는 function을 생성하여 액세스하고자 하는 모든 type을 등록할 수 있다

     

    type은 Factory Singleton 두 가지 방법으로 등록될 수 있다.

     

    Factory method pattern

    팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하여 만든다. 따라서 해당 클래스의 인스턴스를 매번 새로 만들 필요가 없다. 

    Singleton

    최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴.
    해당 인스턴스가 필요할 때 인스턴스를 새로 생성하는 것이 아니라 동일한 인스턴스를 사용하게 한다.

     

    - main.dart

    import 'service_locator.dart';
    
    void main() {
      setupLocator();
      runApp(MyApp());
    }

     

    모든 타입들은 앱을 시작하기 전에 등록되어야한다. 

    setupLocator를 runApp() 전에 불러주면 service provider가 locator에 등록된다. 

     

    - 데이터 접근 

    import 'service_locator.dart';
    ...
    
    @override
      Widget build(BuildContext context) {
        // Request the AppInfo from the locator
        var appInfo = locator<AppInfo>();
        return Scaffold(
          body: Center(
            child: Text(appInfo.welcomeMessage),
          ),
        );
      }

    get it에서 instance를 요청하기 위해서는 widget을 감쌀 필요 없이

    service_locator.dart의 locator로부터 AppInfo의 유형을 확인하면 된다. 

    따라서 전체적인 앱의 UI 구조가 변경되더라도 올바른 서비스와 값을 주입할 수 있다.

     

     

     

    4) provider 

     

    InheritedWidget와 비슷하다.

     

     

    특징

     

    - 한 방향의 데이터 흐름을 강제한다.

    - StateManagement에 최고다.

    - 모든 공급자를 disposing할 수 있는 방법을 제공한다. 

    - 단 BuildContext가 없는 개체에 주입하기 위해서는 ProxyProvider를 많이 써야하는 번거로움이 있다.

     

     

    사용예시

    dependencies:
         provider:^4.3.3

     

    - provider 생성

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Provider(
          builder: (context) => AppInfo(),
          child: MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: Scaffold(),
          ),
        );
      }
    }

    InheritedWidget처럼 Provider의 주입된 값은 하위 트리에서만 사용할 수 있다.

    데이터를 어디에서든 사용할 수 있도록 전체 앱을 provider로 감싼다. 

     

    - 데이터 접근 

     @override
      Widget build(BuildContext context) {
        var appInfo = Provider.of<AppInfo>(context);
        return Scaffold(
          body: Center(
            child: Text(appInfo.welcomeMessage),
          ),
        );
      }

    하위 클래스에서 provider로 부터 접근하고자 하는 AppInfo에 접근할 수 있다. 


    인용 및 참고

     

    - 종속성 주입

    ichi.pro/ko/flutter-gandanhan-jongsogseong-ju-ib-laibeuleoli-60349278189554

    api.flutter.dev/flutter/widgets/InheritedWidget-class.html  

    medium.com/manabie/how-does-flutter-inheritedwidget-work-3123f9d74c15  

     

    - factory 개념

    developside.tistory.com/81

     

    - 싱글톤 개념

    jeong-pro.tistory.com/86

     

    반응형

    댓글

Designed by Tistory.