본문 바로가기
프로그래밍/Flutter

[flutter 상태관리] 9. 플러터 Provider 로 상태(모델) 공유

by 신나요 2022. 6. 13.

Provider는 플러터에서 InheritedWidget을 쉽게 사용할 수 있도록 상태를 관리를 도와주는 라이브러리입니다. 지난 포스트에서는 InheritedWidget과 ScopedModel을 사용해서 상태 관리는 해보았는데요. Provier를 사용하면 얼마나 간단하게 상태 관리를 할 수 있는지 알아보도록 하겠습니다. ScopedModel로 상태 관리를 하고 있는 코드에서 Provider로 상태 관리를 하는 코드로 수정을 해보겠습니다. 전체 코드를 참고 하고 싶으시면 하단으로 내려주세요.

 

*InheritedWidget, ScopedModel 포스트도  같이 읽어주세요

[flutter 상태관리] 7. InheritedWidget 으로 상태 공유하기

[flutter 상태관리] 8. 플러터 ScopedModel


Provider의 역할

Provider의 역할은 당연히 플러터 앱 내에서 상태를 관리해 주는 역할을 합니다. 좀 더 얘기하면 부모 위젯에서 자식 위젯으로 데이터를 전달할 수 있습니다.

위젯 트리를 따라서 연달아 상태를 건네줄 필요 없이 상태가 필요한 위젯에서 BuilderContext를 이용하여 상태에 접근할 수 있습니다. Provider를 사용하면 소스코드를 매우 간단하게 유지할 수 있게 됩니다.


Provider 패키지 설치

Provider는 다트와 플러터를 설치할 때 같이 설치되지 않습니다. ScopedModel과 마찬가지로 Provider를 사용하려면 외부 패키지를 인스톨해줘야 합니다. 외부 라이브러리를 인스톨하기 위해 pubspec.yaml파일에서 의존성을 추가해주시면 됩니다.

의존성을 추가하고 저장하면 vscode에서 인스톨해줍니다.

 

패키지 import 하기

이제 라이브러리를 사용할 다트 파일에서(예제의 경우 main.dart) 아래 임포트 문을 추가하면 provider를 사용할 수 있게 됩니다.

import 'package:provider/provider.dart';

 

상태 클래스 정의 하기

상태는 아래와 같이 정의하였습니다. 우리는 이 상태를 Provider를 사용하여 위젯 전체에 공유하는 것이 목적입니다.

앱 위젯 전체에 공유하고 싶은 상태 클래스인 AppState에 with 키워드로 ChangeNotifier를 함께 사용해 주고 있습니다. ChangeNotifier에는 ScpoedModel과 동일한 notifyListeners메서드가 존재합니다. Stateful 위젯에서는 상태가 변경되었을 때 setState()를 호출하여 위젯들이 갱신되도록 하였는데요. 마찬가지로 notifyListeners는 모델에 의존하는 위젯에 대한 변경을 리스너에게 알려 Provider가 래핑 하는 하위 위젯들을 갱신하도록 합니다.

 

Provider 설정하기(상태를 위젯 트리에 설정)

상태 클래스를 만들었다면 상태 클래스를 상위 위젯 트리에 배치해줘야 합니다. 여러 형태의 프로바이더가 존재하지만 이번 예제에서는 ChangeNotifierProvider로 상태를 설정하게 됩니다. ChangeNotifierProvider는 Listenable 객체를 위한 Provider로 리스너가 호출될 때 오브젝트를 수신하고 위젯을 재구성하도록 요청합니다. 또한 필요할 때 자동으로 dispose를 호출하여 상태의 노출을 해제해 줍니다.  App 클래스에서도 Provider를 설정할 수 있지만 이번 예제에서는 main메소드에서 설정해 보도록 하겠습니다.

ChangeNotifierProvider를 최상위로 설정해 주고 있고 모든 것을 래핑하고 있습니다. ChangeNotifierProvider에는 두 개의 속성을 설정해 주고 있습니다. create에는 공유할 상태를 만들어주는 함수를 설정하고 있고 child에는 앱 자체를 설정해주는데 App이 플러터 앱의 최상위 임으로 App위젯을 설정해 주고 있습니다.  이제 앱의 모든 위젯에서 AppState를 공유하게 될 것입니다. 만약 AppState 클래스가 with 키워드로 ChangeNotifier를 사용하고 있지 않다면 ChangeNotifierProvider 생성에서 에러가 날 것입니다.

 

위젯에서 상태에 접근하기

상태가 공유되도록 설정을 마쳤고, 이제 상태를 하위 위젯에서 접근하여 사용해 보겠습니다. 아래 코드는 하위 위젯의 빌드 메서드입니다.

하위 위젯에서 상태에 접근할 때는 context.watch, context.read, context.select를 이용하여 상태에 접근할 수 있습니다.

1. GridView의 children속성에 Photo 위젯 리스트를 설정해주고 있습니다. 상태에 있는 photoStates을 루프를 돌며 개수만큼 리스트를 만들게 되는데요. context.watch<AppState>()를 사용해서 생성한 AppState의 상태 객체에 접근을 할 수 있게 됩니다. watch를 사용하게 되면 상태의 변화를 관찰하게 되는데, notifyListeners가 호출되면 context.watch를 사용하는 위젯에 변경사항을 전파하여 위젯을 갱신할 수 있습니다. 즉 watch는 갱신이 필요한 위젯에 사용합니다.

2. 반면 read는 상태를 관찰하지 않습니다. notifyListeners가 호출돼도 read로 접근하여 생성되는 위젯은 업데이트되지 않습니다.

예제에서는 사용하지 않았지만 select를 사용하면 상태의 특정 오브젝트의 변화가 있을 때만 위젯을 갱신합니다.


다른 하위 위젯에서도 Provider를 사용하는 상태 공유로 수정을 해주었고, 수정된 전체 코드는 아래에 첨부하였습니다. 앱의 동작은 변함이 없지만 Provider를 사용해 상태를 관리해줌으로 종속성을 없앨 수 있었고 코드도 훨씬 깔끔해졌습니다.

 


여기까지 플러터 앱에서 상태를 공유하는 세 가지 방법을 다뤄보았습니다. InheritedWIdget, ScopedModel, Provider에 대해 다뤘었는데요. 대부분의 플러터 앱을 만들 때는 세 가지중 Provider를 사용하시면 해결됩니다. 이번 포스트에서 보았듯이 Provider는 매우 깨끗한 구문을 가지고 있습니다. 최상위에서 Provider를 정의하고 하위 위젯에서 BuildContext를 사용하여 watch, read 등을 사용하면 됩니다.

하지만 이게 모든 건 아닙니다. 상태 관리를 다루는 여러 라이브러리 들과 개념들이 존재합니다. 차차 포스트해보도록 하겠습니다.

 

--전체 코드--

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(create: (_) => AppState(), child: const App())
  );
}

const List<String> urls = [
  "https://cdn.pixabay.com/photo/2017/06/29/13/29/pug-2454270_1280.jpg",
  "https://cdn.pixabay.com/photo/2014/09/19/21/47/chihuahua-453063_1280.jpg",
  "https://cdn.pixabay.com/photo/2015/06/15/23/56/chihuahua-810789_1280.jpg",
  "https://cdn.pixabay.com/photo/2019/04/02/16/11/cat-4098058_1280.jpg"
];

class PhotoState {
  String url;
  bool selected;
  bool display;
  Set<String> tags = {};

  PhotoState(this.url, {this.selected = false, this.display = true});
}

class AppState with ChangeNotifier {
  bool isTaggingMod = false;
  List<PhotoState> photoStates = List.of(urls.map((url) => PhotoState(url)));
  Set<String> tags = {"all", "dog", "cat"};
  
  void toggleTagging(url) {
    isTaggingMod = !isTaggingMod;
    for (var element in photoStates) {
      if (isTaggingMod && element.url == url) {
        element.selected = true;
      } else {
        element.selected = false;
      }
    }
    notifyListeners();
  }

  void selectTag(String tag) {
    if (isTaggingMod) {
      if (tag != "all") {
        for (var element in photoStates) {
          if (element.selected) {
            element.tags.add(tag);
          }
        }
        toggleTagging(null);
      }
    } else {
      for (var element in photoStates) {
        element.display = tag == "all" ? true : element.tags.contains(tag);
      }
    }
    notifyListeners();
  }

  void onPhotoSelect(String url, bool selected) {
    for (var element in photoStates) {
      if (element.url == url) {
        element.selected = selected;
      }
    }
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
          title: 'Photo',
          theme: ThemeData(
            primarySwatch: Colors.blueGrey,
            unselectedWidgetColor: Colors.grey,
          ),
          home: const Gallery(title: 'Gallery')
          );
  }
}

class Gallery extends StatelessWidget {
  final String title;

  const Gallery({required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: GridView.count(
            crossAxisCount: 2,
            mainAxisSpacing: 5.0,
            crossAxisSpacing: 5.0,
            padding: const EdgeInsets.all(5.0),
            children: List.of(
              context.watch<AppState>().photoStates.where((ps) => ps.display).map((ps) => Photo(
                  state: ps
            )))),
      drawer: Drawer(
          child: ListView(
        children: List.of(context.watch<AppState>().tags.map((t) => ListTile(
              title: Text(t),
              onTap: () {
                context.read<AppState>().selectTag(t);
                Navigator.of(context).pop();
              },
            ))),
      )),
    );
  }
}

class Photo extends StatelessWidget {
  final PhotoState state;

  const Photo({required this.state});

  @override
  Widget build(BuildContext context) {
    List<Widget> children = [
      GestureDetector(
        child: Image.network(state.url),
        onLongPress: () => context.read<AppState>().toggleTagging(state.url),
      )
    ];

    if (context.watch<AppState>().isTaggingMod) {
      children.add(Positioned(
        left: 0,
        top: 0,
        child: Checkbox(
          onChanged: (value) {
            context.read<AppState>().onPhotoSelect(state.url, value!);
          },
          value: state.selected,
          activeColor: Colors.white,
          checkColor: Colors.black45,
        ),
      ));
    }

    return Container(
        child: Stack(
      alignment: Alignment.topLeft,
      children: children,
    ));
  }
}

댓글