본문 바로가기
flutter

플러터 - Provider

by 들풀민들레 2023. 3. 13.

 

본 글은 [Do it! 깡샘의 플러터&다트 프로그래밍] 의 내용을 발췌한 것입니다.

 

 

책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.

 

앞서 살펴본 것처럼 Provider()나 Provider.value()로 상태를 등록하고 하위 위젯에서 Provider.of()로 프로바이더의 상태를 이용할 수 있습니다. 그런데 앱의 상태 데이터는 다양한 타입일 수 있으며 여러 개를 등록할 수도 있습니다. 이를 지원하는 다양한 프로바이더를 제공합니다.


변경된 상태를 하위 위젯에 적용하기 — ChangeNotifierProvider


프로바이더를 이용해 상태 데이터를 하위 위젯에서 이용할 수 있지만, 프로바이더에 등록된 상태 데이터는 값이 변경되더라도 하위 위젯이 다시 빌드하지 않으므로 변경 사항이 적용되지 않습니다. 만약 변경된 상태 데이터를 하위 위젯에 적용하려면 ChangeNotifierProvider를 이용합니다. ChangeNotifierProvider에 등록하는 상태 데이터는 ChangeNotifier를 구현해야 합니다. 따라서 int 등 기초 타입의 상태는 등록할 수 없습니다.
다음 코드에서 Counter 클래스는 ChangeNotifierProvider에 등록하여 하위 위젯에서 이용할 데이터를 추상화한 모델 클래스입니다.

class Counter with ChangeNotifier {
    int _count = 0;
    int get count => _count;
    void increment() {
        _count++;
        notifyListeners();
    }
}

이런 클래스는 with ChangeNotifier로 선언해야 합니다. 이 예에서는 어디선가 increment() 함수를 호출하여 상태 데이터를 변경한다고 가정합니다. 그런데 단순하게 상태 데이터가 변경됐다고 해서 하위 위젯을 다시 빌드하지는 않으며, notifyListeners() 함수를 호출해 주어야 합니다. 즉, 상태 데이터가 변경된 후 변경 사항을 적용하려면 notifyListeners() 함수를 호출해야 합니다.
ChangeNotifierProvider는 자신에게 등록된 모델 클래스에서 notifyListeners() 함수 호출을 감지해 child에 등록된 위젯을 다시 빌드해 줍니다.

ChangeNotifierProvider<Counter>.value(
    value: Counter(),
    child: SubWidget(),
)

멀티 프로바이더 등록하기 — MultiProvider


앱의 상태 데이터는 여러 가지입니다. 예를 들어 쇼핑몰과 관련된 앱을 만든다고 하면 앱에서 유지해야 할 상태 데이터는 고객 정보, 상품 정보, 주문 정보 등 다양합니다. 프로바이더는 일반적으로 하나의 의미 단위로 만들므로 각각의 데이터를 등록하는 프로바이더가 여러 개 필요합니다. 이처럼 여러 프로바이더를 한꺼번에 등록해서 이용할 때 하나의 프로바이더 위젯에 다른 프로바이더를 등록하여 계층 구조로 만들 수 있습니다.
다음 코드에서는 프로바이더를 3개 등록했습니다. Provider<int>의 child에 Provider <String>을 등록했고 다시 Provider<String>의 child에 ChangeNotifierProvider<Counter>를 등록했습니다.

Provider<int>.value(
    value: 10,
    child: Provider<String>.value(
        value: "hello",
        child: ChangeNotifierProvider<Counter>.value(
            value: Counter(),
            child: SubWidget(),
        )
    ),
)

이처럼 프로바이더의 계층 구조로 여러 프로바이더를 등록하고 사용할 수도 있지만 Multi Provider를 이용하면 조금 더 쉽고 읽기 좋은 코드를 작성할 수 있습니다. MultiProvider는 providers 속성을 제공하며 이 속성에 여러 프로바이더를 배열로 등록할 수 있습니다. 앞의 예를 MultiProvider를 이용해 작성하면 다음과 같습니다.

MultiProvider(
    providers: [
        Provider<int>.value(value: 10),
        Provider<String>.value(value: "hello"),
        ChangeNotifierProvider<Counter>.value(value: Counter()),
    ],
    child: SubWidget()
)

프로바이더를 여러 개 등록할 때 타입 중복 문제가 발생할 수 있습니다. 다음 코드는 프로바이더를 5개 등록한 예입니다.

MultiProvider(
    providers: [
        Provider<int>.value(value: 10),
        Provider<String>.value(value: "hello"),
        ChangeNotifierProvider<Counter>.value(value: Counter()),
        Provider<int>.value(value: 20),
        Provider<String>.value(value: "world"),
    ],
    child: SubWidget()
)

각기 다른 데이터를 5개의 프로바이더로 등록했습니다. 그런데 int 제네릭 타입의 프로바이더가 2개, String 제네릭 타입의 프로바이더가 2개입니다. 하위 위젯이 프로바이더를 이용할 때는 제네릭 타입으로 이용하므로 같은 제네릭 타입으로 등록하면 마지막에 등록한 프로바이더를 이용하게 됩니다. 다음은 앞에서 등록한 프로바이더를 이용하는 하위 위젯의 코드입니다.

Widget build(BuildContext context) {
    var counter = Provider.of<Counter>(context);
    var int_data = Provider.of<int>(context);
    var string_data = Provider.of<String>(context);
    return Row(
        children: <Widget>[
            Text('Provider : '),
            Text('int : $int_data, string : $string_data, count : ${counter.count}'),
            ElevatedButton(
                child: Text('increment'),
                onPressed:() {
                    counter.increment();
                },
            )
        ],
    );
}

 

상태 조합하기 — ProxyProvider


ProxyProvider는 상태를 조합할 때 사용합니다. 여러 프로바이더로 상태를 여러 개 선언할 때 각각의 상태를 독립적으로 이용할 수도 있지만, 어떤 상탯값을 참조해서 다른 상탯값이 결정되게 할 수도 있습니다. 또한 한 상탯값이 변경되면 다른 상탯값도 함께 변경해야 할 때도 있습니다. 이때 참조 상태를 다른 상태에 전달해야 하는데 이를 쉽게 구현하도록 ProxyProvider를 제공합니다.
ProxyProvider에는 제네릭 타입을 2개 선언합니다. 예를 들어 ProxyProvider<A, B>로 선언한다면 A는 전달받을 상태 타입이며, B는 A를 참조해서 만들 상태 타입이 됩니다.
다음 코드에서는 프로바이더를 2개 등록했습니다. 첫 번째 등록한 ChangeNotifierProvideer <Counter>는 Counter를 상태로 등록하는 프로바이더입니다. 두 번째 등록한 ProxyProvider <Counter, Sum>은 Counter 상태를 전달받아 Sum 상태를 등록하는 프로바이더입니다.
ProxyProvider를 이용하면 update 속성에 함수를 등록해야 하는데, 이 함수에서 반환된 값이 상태로 등록됩니다. 예에서는 update에 등록한 함수에서 Sum 객체를 반환하므로 이 객체가 상태로 등록됩니다. 그런데 update 함수를 호출할 때 두 번째 매개변수에 Counter 객체가 전될됩니다. 결국 두 번째 매개변수로 전달된 상태를 참조하여 자신의 상태를 만들게 됩니다.

MultiProvider(
    providers: [
        ChangeNotifierProvider<Counter>.value(value: Counter()),
        ProxyProvider<Counter, Sum>(
            update: (context, model, sum) {
                return Sum(model);
            }
        )
    ],
    child: SubWidget()
)

상태를 전달받아 다른 상태를 만들 때 전달받는 상태가 여러 개일 수도 있습니다. 즉, 여러 개의 상태를 참조해서 하나의 상태를 만들 수도 있습니다. 이에 따라 ProxyProvider는 전달받는 상태 개수에 따라 ProxyProvider2~ProxyProvider6까지 제공합니다.
다음 코드는 ProxyProvider2를 이용한 예인데 3개의 제네릭 타입을 선언했습니다. 앞의 2개(Counter, Sum)는 전달받는 상태의 제네릭 타입이며, 마지막은 앞 2개의 상태를 참조해서 만들 상태의 제네릭 타입입니다. 또한 ProxyProvider2를 이용했으므로 update의 함수도 Proxy Provider의 update 함수보다 매개변수가 하나 더 많습니다. 두세 번째 매개변수로 참조할 상태가 전달됩니다.

ProxyProvider2<Counter, Sum, String>(
    update: (context, model1, model2, data) {
    	return "${model1.count}, ${model2.sum}";
    }
)

ProxyProvider의 생명주기
프로바이더에 등록한 상태 객체는 싱글톤으로 운영됩니다. 처음에 객체가 생성되면 그 객체의 데이터가 변경되는 것이지, 객체가 다시 생성되지는 않습니다. 그런데 ProxyProvider에 등록한 상태 객체는 데이터가 변경될 때마다 객체가 다시 생성될 수 있습니다.

만약 다음처럼 2개의 프로바이더를 등록했다면 첫 번째 ChangeNotifierProvider에 등록한 Counter 객체는 처음 한 번만 생성됩니다. 하지만 ProxyProvider에 등록한 Sum 객체는 Counter값이 변경될 때마다 반복해서 생성됩니다. ProxyProvider는 다른 상태를 참조하여 새로운 상태를 만드는 것이므로 참조하는 상태가 변경되면 그 값을 반영하여 새로운 상태가 만들어져야 합니다. 참조하는 상태가 변경될 때마다 update에 지정한 함수가 자동으로 호출되며 변경된 값이 두 번째 매개변수로 전달됩니다.

MultiProvider(
    providers: [
        ChangeNotifierProvider<Counter>.value(value: Counter()),
        ProxyProvider<Counter, Sum>(
            update: (context, model, sum) {
            	return Sum(model);
            }
        ),
    ],
    child: SubWidget()
)

그런데 ProxyProvider를 이용하더라도 상태 객체가 매번 생성될 필요가 없을 수도 있습니다. update에 등록한 함수가 매번 호출되더라도 이전 상태 객체를 그대로 이용하면서 상탯값만 바꾸는 것이 효율적일 때가 있습니다. 이때는 update에 등록한 함수의 세 번째 매개변수로 이전에 이용했던 상태 객체를 전달해 줍니다. 결국 세 번째 매개변수를 활용하여 객체를 다시 생성할 것인지, 아니면 기존 객체를 이용하여 값만 변경할 것인지를 적절하게 결정하면 됩니다.

ProxyProvider<Counter, Sum>(
    update: (context, model, sum) {
        if (sum != null) { // 상탯값만 갱신
            sum.sum = model.count;
            return sum;
        } else { // 새로운 객체 생성
            return Sum(model);
        }
    }
),

퓨처 데이터 상태 등록하기 — FutureProvider


FutureProvider는 Future로 발생하는 데이터를 상태로 등록하는 프로바이더입니다. 상태 데이터가 미래에 발생할 때 사용합니다. FutureProvider의 create에 지정한 함수에서 Future 타입을 반환하면 미래에 발생하는 데이터를 상태로 등록합니다.
다음 코드에서 초기에 등록되는 상탯값은 initialData에 지정한 “hello”입니다. 그런데 create에 지정한 함수에서 Future를 반환하므로 미래에 데이터가 발생하면 그 데이터로 상탯값을 변경합니다. 예에서는 초기 상탯값이 “hello”에서 4초 후에 “world”로 바뀝니다.

FutureProvider<String>(
    create: (context) => Future.delayed(Duration(seconds: 4), () => "world"),
    initialData: "hello"
),

FutureProvider의 상태를 이용하는 하위 위젯은 상태 데이터가 미래에 발생하더라도 이를 이용하는 데는 다른 프로바이더와 차이가 없습니다. FutureProvider의 상태를 이용하는 하위 위젯을 다음처럼 작성했다면 처음에는 “future : hello”가 출력되며, 4초 후에 자동으로 “future : world”로 바뀝니다.

var futureState = Provider.of<String>(context);
    return Column(
    children: <Widget>[
    	Text('future : ${futureState}'),
    ],
);

스트림 데이터 상태 등록하기 — StreamProvider


StreamProvider는 Stream으로 발생하는 데이터를 상태로 등록할 때 사용합니다. 다음처럼 데이터를 만드는 스트림 함수가 있다고 가정해 보겠습니다.

Stream<int> streamFun() async* {
    for (int i = 1; i <=5; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i;
    }
}

이 함수에서 만드는 데이터를 프로바이더를 이용해 상태로 등록하려면 다음처럼 Stream Provider를 이용합니다. StreamProvider의 initialData에 초기 상탯값을 등록하고 create에 등록하는 함수에서 스트림을 반환하면 이 스트림에서 발생하는 데이터들을 상탯값으로 등록합니다.

StreamProvider<int>(
    create: (context) => streamFun(),
    initialData: 0
)

StreamProvider의 상태를 이용하는 하위 위젯에서는 상태 데이터가 스트림으로 발생하더라도 이를 이용하는 데는 다른 프로바이더와 차이가 없습니다. StreamProvider의 상태를 이용하는 하위 위젯을 다음처럼 작성했다면 처음에는 initialData에 지정한 값이 출력되고, 이후 스트림이 새로운 값을 만들면 그 값이 자동으로 출력됩니다.

var streamState = Provider.of<int>(context);
return Column(
    children: <Widget>[
    	Text('stream : ${streamState}'),
    ],
);

 

 

 

책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.

 

 

 

'flutter' 카테고리의 다른 글

플러터 - GetX로 상태 관리하기  (0) 2023.03.13
플러터 - Bloc Cubit  (0) 2023.03.13
플러터 - InheritedWidget  (0) 2023.03.13
플러터 - Isolate  (0) 2023.03.13
플러터 - Future, FutureBuilder  (0) 2023.03.13