프로그래밍/Flutter & Dart

[Flutter] state management(provider)

*%$@$#@ 2022. 12. 27.
728x90
반응형

 

앞으로 몇 차례 포스팅을 통해서 provider에 대해서 공부를 해 보고자 합니다.  state management는 기본적으로 app을 흐르는 데이터의 관리라고 생각하면 되겠습니다. 처음 앱을 만들게 되면 페이지를 구성하게 되고 페이지를 구성하거나 사용자의 입력을 받아서 다른 페이지에 전달해야 하는 경우가 생깁니다. 전달하는 데이터가 바로 다음 페이지일 수도 있고 아니면 구조 상 멀리 떨어진 페이지일 수도 있습니다. 

 

이때 필요한 데이터를 제 때 필요한 곳에 전달하는 것이 state management라고 할 수 있으며, 이를 구현하기 위해서는 다음과 같은 몇 가지 방법을 이용할 수 있습니다. 

 

1. stateful widget을 이용한 데이터 전달 

2. inherited widget을 이용한 데이터 관리 

3. provider 이용 

 

이 외에도 BloC, GetX 등 다양하게 방법이 존재하겠으나 시작하는 입장에서 구분해서 알아둬야 하는 것은 위의 세 가지 정도가 될 거 같습니다. 그중에서도 이번 포스팅을 통해서 방법을 알아보고자 하는 것은 3번의 provider입니다. 

 

1번의 stateful widget을 이용하는 것은 widget간 이동 시 heirarchy 트리를 통해 순차적으로 데이터를 이동하는 것을 의미합니다. 나이가 조금 있으신 분들은 TV프로 가족오락관의 '방과 방 사이'라는 코너를 기억할 것입니다. 제일 뒷사람이 단어를 확인한 뒤 손짓 발짓으로 앞사람에게 단어를 설명하면서 가장 앞사람에게 순차적으로 단어를 전달하는 게임인데요. 이처럼 부모 위젯이 자녀 위젯을 호출하면서 필요한 데이터도 같이 전달합니다. 

 

함수의 인자처럼 데이터를 전달하기에 가장 간편하고 직관적인 방법이라 할 수 있습니다. 따라서 간단한 앱을 구동할 때에는 쉽게 사용할 수 있습니다. 하지만 앱의 구조가 복잡해지면 데이터를 전달하기 위해 여러 단계를 거쳐야 할 수도 있으며, 이 때문에 오히려 앱이 더욱 복잡해 질 수 있습니다. 

 

이를 보완하는 방법이 바로 2번과 3번의 inherited widget과 provider입니다. 

 

둘 다 데이터를 필요로하는 위젯들의 가장 상위에 상태관리 위젯을 만들고 데이터가 필요한 위젯들은 상위의 상태관리 위젯을 통해 데이터를 얻는 방법입니다. Flutter는 자신의 하위 위젯(자녀뿐 아니라 손자 그 아래 위젯에게도)에게 데이터를 context를 이용해서 전달할 수 있습니다. 이를 이용한 상태관리 방법입니다. inherited widget과 provider는 비슷한 구조로 동작합니다. inherited widget은 flutter SDK에서 기본적으로 제공하는 기능이라고 보시면 되고, provider는 이를 개선한 3rd party 패키지로 생각하면 되겠습니다. inherited widget에 비해서 provider의 사용이 워낙에 간편하고 기능이 강력해서 flutter팀에서도 공식적으로 provider의 사용을 권장하고 있습니다.

 

provider의 개념은 라디오 방송과 비슷하다고 볼 수 있습니다. 라디오 방송국은 각각 별도의 주파수로 방송을 송출하고 이에 청취자는 자신이 원하는 주파수에 맞춰 방송을 듣게됩니다. 여기서 라디오 방송국은 provider 모델로 비유할 수 있고 다른 위젯은 필요한 provider 모델로부터 데이터를 전달받게 됩니다. 

 

그럼 좀 더 자세히 살펴보도록 하겠습니다. 먼저 Flutter 공식 문서를 통해서 정보를 얻어 보도록 하겠습니다.
https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple

 

Simple app state management

A simple form of state management.

docs.flutter.dev


예제 프로그램의 작동 영상입니다. 아래 어플은 크게 두 개의 페이지(위젯)으로 구성되어 있습니다. MyCatalog 위젯과 MyCart위젯으로 구분할 수 있으며, MyCatalog 위젯 하위에는 여러 개의 MyListItem 위젯으로 구성되어 있습니다. 

 

MyCatalog위젯에서 MyListItem위젯을 선택하면 그 선택한 결과가 MyCart 위젯에 전달되어 아이템 개수를 기반으로 최종 가격을 계산하는 간단한 기능의 위젯입니다. 




각 위젯들의 관계를 그려보면 다음과 같습니다. 



먼저 MyCatalog 페이지에는 커스텀으로 제작된 MyAppBar라는 위젯이 있고, 그 아래 body에는 수많은 상품 리스트(MyListItems)가 있습니다. 우리는 아이템을 선택해서 카트에 담고자 합니다. MyCatalog 페이지에서 아이템을 선택할 때 이미 카트에 담겨 있는 아이템의 목록 또한 같이 보여주면 좋겠습니다.

이를 Provider를 이용해서 구현해 보도록 하겠습니다. 

 

Provider를 이용하신다면 3가지를 기억해야 합니다.

1. ChangeNotifier
2. ChangeNotifierProvider
3. Consumer

 

dartpad를 이용해서 위의 예제를 실행해 보았습니다. 아래 링크를 통해서 실행해 보세요. 

 

https://dartpad.dev/?id=e0addaa02dbe34ea806e4ead0600f454 

 

DartPad

 

dartpad.dev

 

 



1. ChangeNotifier

ChangeNotifier는 Flutter SDK에 기본적으로 내장되어 있는 class로써 상태정보를 받아보는 위젯(listener)들에게 상태 변경 정보를 전달할 수 있도록 해줍니다. 예제에는 CartModel이 ChangeNotifier로 관리되고 있습니다. 

 

아래 코드를 보면 catalog와 add 및 remove에 notifyListeners();가 설정되어 있는 것을 볼 수 있습니다. 이를 통해서 상태 변화가 발생하였을 때 UI를 새롭게 그릴 수 있게 됩니다. 

class CartModel extends ChangeNotifier {
  /// The private field backing [catalog].
  late CatalogModel _catalog;

  /// Internal, private state of the cart. Stores the ids of each item.
  final List<int> _itemIds = [];

  /// The current catalog. Used to construct items from numeric ids.
  CatalogModel get catalog => _catalog;

  set catalog(CatalogModel newCatalog) {
    _catalog = newCatalog;
    // Notify listeners, in case the new catalog provides information
    // different from the previous one. For example, availability of an item
    // might have changed.
    notifyListeners();
  }

  /// List of items in the cart.
  List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();

  /// The current total price of all items.
  int get totalPrice =>
      items.fold(0, (total, current) => total + current.price);

  /// Adds [item] to cart. This is the only way to modify the cart from outside.
  void add(Item item) {
    _itemIds.add(item.id);
    // This line tells [Model] that it should rebuild the widgets that
    // depend on it.
    notifyListeners();
  }

  void remove(Item item) {
    _itemIds.remove(item.id);
    // Don't forget to tell dependent widgets to rebuild _every time_
    // you change the model.
    notifyListeners();
  }
}

2. ChangeNotifierProvider

 

다음으로 필요한 것은 ChangeNotifierProvider를 설정하는 것입니다. 앞서 본 ChangeNotifier 인스턴스를 하위 위젯들에게 제공하는 역할을 합니다. 앞서 ChagneNotifierProvider는 이를 이용하는 위젯들의 상위에 위치하고 있어야 합니다. 가장 최상위에 위치할 필요는 없으나 shopper 예제에서는 MyCart와 MyCatalog의 상위인 main에 설정이 되어 있습니다. 

 

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

  @override
  Widget build(BuildContext context) {
    // Using MultiProvider is convenient when providing multiple objects.
    return MultiProvider(
      providers: [
        // In this sample app, CatalogModel never changes, so a simple Provider
        // is sufficient.
        Provider(create: (context) => CatalogModel()),
        // CartModel is implemented as a ChangeNotifier, which calls for the use
        // of ChangeNotifierProvider. Moreover, CartModel depends
        // on CatalogModel, so a ProxyProvider is needed.
        ChangeNotifierProxyProvider<CatalogModel, CartModel>(
          create: (context) => CartModel(),
          update: (context, catalog, cart) {
            if (cart == null) throw ArgumentError.notNull('cart');
            cart.catalog = catalog;
            return cart;
          },
        ),
      ],

예제에는 Provider의 한 종류인 MultiProvider 및 ChangeNotifierProxyProvider가 적용되어 있습니다. 여러 개의 Provider를 관리하는 방법으로 인해하면 되겠습니다. 

 

3. Consumer

 

이제 CartModel은 ChagneNotifierProvider를 통해서 위젯들이 이용할 수 있도록 정의되었습니다. 이제 Consumer를 이용해서 이를 이용하면 됩니다. 

 

아래의 코드를 보면 Row의 children 중 하나로 Consumer 위젯이 정의된 것을 볼 수 있습니다. Consumer위젯은 모델 타입을 정의해 줘야 하는데(여기서는 CartModel) 이는 앞서 라디오 방송국의 예시에서의 주파수를 맞추는 개념과 같습니다. 

 

Consumer는 builder를 필수로 설정해 주어야 하는데, 이 builder는 ChangeNotifier가 변경되면 항상 실행되는 함수입니다. 다시 이야기하면 CartModel안에 있는 notifyListener()가 호출되면 Consumer 위젯에 있는 builder가 실행된다고 이야기할 수 있습니다. 

 

builder 함수는 세 개의 인자를 전달받는데, 모든 builder에서 볼 수 있는 context와 ChangeNotifier 인스턴스인 cart, 그리고 최적화를 위한 child 위젯이 있습니다. Consumer가 동작할 때 변경되지 않는 부분을 child로 별도로 분리하여 메모리를 절약할 수 있습니다. 가장 좋은 방법은 Consumer를 변경이 필요한 부분만 최소한으로 감싸는 것이 좋습니다. 

return SizedBox(
      height: 200,
      child: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Another way to listen to a model's change is to include
            // the Consumer widget. This widget will automatically listen
            // to CartModel and rerun its builder on every change.
            //
            // The important thing is that it will not rebuild
            // the rest of the widgets in this build method.
            Consumer<CartModel>(
                builder: (context, cart, child) =>
                    Text('\$${cart.totalPrice}', style: hugeStyle)),
            const SizedBox(width: 24),
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Buying not supported yet.')));
              },
              style: TextButton.styleFrom(foregroundColor: Colors.white),
              child: const Text('BUY'),
            ),
          ],
        ),
      ),
    );

위와 같이 설정하면 CartModel이 변경될 경우 Consumer가 호출되고 이로 인해서 업데이트된 cart.totalPrice를 적용한 UI가 rebuild 되게 됩니다. 

 

728x90
반응형

댓글