Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-05 13:14
관리 메뉴

nomad-programmer

[Programming/Flutter] Provider Pattern 본문

Programming/Flutter

[Programming/Flutter] Provider Pattern

scii 2020. 10. 12. 00:23

Flutter Provider란?

Provider는 2019년 구글 IO에서 추천되며 큰 주목을 받았다. 원래는 플러터 커뮤니티에서 만든 플러그인이었으나, 구글에서 공식적으로 추천할 정도로 편리함을 가져다준다.

2018년 구글 IO까지만 해도 구글은 Provider가 아닌 BloC 패턴 사용을 권장했었다. 플러터는 UI와 Design 모두 소스코드로 관리되지 않으면 한 클래스에 여러 코드가 몰리게 되는 문제가 있었다. 이를 해결하기 위해 UI와 데이터 처리 로직 분리가 되는 BloC 패턴을 제공했다. 하지만 BloC 패턴은 사용하기 너무 어렵다는 말이 있었고, 단순한 로직을 구성하려해도 최소 4개의 클래스를 만들어야만 했다. 반면, Provider 패턴을 사용하면 데이터 공유나 로직의 분리를 좀 더 간단히 할 수 있게 된다.

medium.com/flutter-community/flutter-architecture-provider-implementation-guide-d33133a9a4e8

 

Flutter Architecture — Provider Implementation Guide

A complete guide to setting up a production ready architecture using Provider for state management in Flutter.

medium.com


Provider Pattern을 사용하는 이유

1. 관심사의 분리

보통 관심사는 어떤 코드가 수행하는 일을 의미한다. UI 담당 코드, 네트워크 담당 코드, 데이터 IO 담당 코드 등 역할에 따라 코드를 나눌 수 있다.
일반적으로 한 개의 클래스가 여러 역할을 수행하면 할수록 방대해지고 관리가 어렵게 된다. 그렇기에 클래스가 하나의 역할(관심)만 갖도록 클래스를 나눈다. 이를 관심사의 분리하고 하며, Provider나 BloC 패턴 사용 이유의 주 목적이다.

2. 데이터 공유의 원활함

하나의 데이터를 여러 페이지에서 간편하게 공유 할 수 있다. 예를 들어 user 인증 정보의 경우 회원 정도, 장바구니 등 여러 페이지에서 사용된다. 이를 페이지마다 인증 정보를 불러오게되면 앱이 복잡해지며 그 만큼 비용도 많이 든다. 이럴 때 데이터 공유가 필요하다. Provider 패턴은 데이터 공유를 쉽게 할 수 있도록 도와준다.

3. 간결한 코드

BloC 패턴의 경우 클래스들을 역할별로 나누는데 좋다. 하지만 코드 자체가 복잡해지는 경향이 존재한다. Provider 패턴을 사용하면 좀 더 적은 코드로 클래스를 구분하여 사용할 수 있다. 

구글에서는 중규모 프로젝트는 Provider 패턴을, 대규모 프로젝트는 BloC 패턴 사용을 권장한다.

Provider 구조

Provider의 구성 요소는 데이터를 생산하고 소비하는 2가지로 구분된다. 또한 어떤 데이터를 생산하느냐에 따라 Provider의 종류도 달라진다. 상황에 따라 일반적인 Provider와 실시간으로 변경되는 StreamProvider가 되기도 한다.

Provider 라이브러리 

pub.dev/packages/provider

 

provider | Flutter Package

A wrapper around InheritedWidget to make them easier to use and more reusable.

pub.dev

Provider 데이터 생산 코드

Provider<int>.value(
  value: 10,
  child: Container(),
)

데이터 생산에는 데이터 타입(자료형)을 정의해야 한다. 위의 생산 코드에서는 int 형태의 데이터로 정의한다.

Provider 데이터 소비 코드

int data = Provider.of<int>(context);

Provider에서 제공하는 데이터를 사용하기 위해서는 Provider.of(context) 혹은 Consumer() 위젯을 사용한다.


Provider 사용 예제

Provider 데이터 생산 코드

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<int>.value(
      value: 10,
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          // primarySwatch: Colors.blue,
          brightness: Brightness.dark,
          primaryColor: Colors.lightBlue[800],
          accentColor: Colors.cyan[600],
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(
          title: '플러터 테스트',
        ),
      ),
    );
  }
}

Provider를 사용하려면 부모 위젯을 Provider로 감싸주어야 한다. 위의 코드처럼 전체 앱에서 사용하고 싶다면 MaterialApp을 Provier로 감싸주면 된다. 위의 코드 예제는 간단히 특정 숫자 10을 넣어 사용하였다. 하지만 일반적으로 변수를 많이 사용한다.

Provider 데이터 소비 코드

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    // provider 소비 코드
    var data = Provider.of<int>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('provider test'),
      ),
      body: Text(
        '$data',
        style: Theme.of(context).textTheme.headline3,
      ),
    );
  }
}

Provider.of(context)로 data 변수에 Provider 데이터를 가져온다. data 변수에 들어간 값은 10이다. 부모 위젯에서 정한 값이 들어가게 된다.
이렇게 데이터를 만드는 곳과 사용하는 곳이 분리되어 코드 관리가 수월해진다.


ChangeNotifierProvider 사용

플러터에서 일반적으로 UI의 값이 변경되었을 때, setState를 이용하여 페이지를 다시 그린다. 하지만 Provider에서는 ChangeNotifier를 사용하여 같은 일을 수행할 수 있다.

ChangeNotifier를 믹스인(Mixin)한 클래스는 notifyListeners() 함수를 호출할 수 있게 되고, 해당 함수를 사용 시 UI가 업데이트 된다.

다음 코드를 통해 counter 값이 변경될 때 화면을 갱신하는 클래스를 알아보자.

class Counter with ChangeNotifier {
  int _counter = 0;

  Counter(this._counter);

  // _counter 변수 값을 가져올 counter getter 메소드
  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void decrement() {
    _counter--;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void reset() {
    _counter = 0;
    notifyListeners();
  }
}

ChangeNotifier가 믹스인(Mixin)된 Counter 클래스를 만들어준다. int 타입의 counter 변수 생성과 파라미터를 받도록 생성자를 정의한다. 또한 동작 시, counter 변수의 값을 변경하고 notifyListeners 함수를 이용하여 UI를 업데이트하는 increment와 decrement 함수를 생성한다.

프로젝트를 새로이 생성하면 생기는 Counter App예제를 Provider로 변경하여 만들어보았다.

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // primarySwatch: Colors.blue,
        brightness: Brightness.dark,
        primaryColor: Colors.lightBlue[800],
        accentColor: Colors.cyan[600],
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      // ChangeNotifierProvider 위젯으로 감싸준다.
      home: ChangeNotifierProvider<Counter>(
        create: (context) => Counter(0),
        child: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider 테스트'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              'Count Number',
              style: Theme.of(context).textTheme.headline4,
            ),
            // Provider 값을 가져온다. Consumer를 사용했다.
            Consumer<Counter>(
              builder: (context, value, child) => Text(
                value.counter.toString(),
                style: Theme.of(context).textTheme.headline2,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: Stack(
        children: <Widget>[
          Positioned(
            right: 10,
            bottom: 120,
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                // setState() 메소드를 호출하지 않아도 화면이 업데이트된다.
                // 이 말인즉슨, stateless 클래스에서도 화면 갱신을 할 수 있다는 뜻이다.
                // listen 매개변수는 플러터 프레임워크에 변경된 것을 알릴지 설정하는
                // 매개변수이다. 헌데, 실제 변경되는것은 Counter.counter 변수이므로
                // listen을 false로 주었다.
                Provider.of<Counter>(context, listen: false).increment();
              },
            ),
          ),
          Positioned(
            right: 10,
            bottom: 40,
            child: FloatingActionButton(
              child: Icon(Icons.horizontal_split),
              onPressed: () {
                Provider.of<Counter>(context, listen: false).decrement();
              },
            ),
          ),
          Positioned(
            left: 40,
            bottom: 40,
            child: FloatingActionButton(
              backgroundColor: Colors.red,
              child: Icon(Icons.repeat),
              onPressed: () {
                Provider.of<Counter>(context, listen: false).reset();
              },
            ),
          ),
        ],
      ),
    );
  }
}

class Counter with ChangeNotifier {
  int _counter = 0;

  Counter(this._counter);

  // _counter 변수 값을 가져올 counter getter 메소드
  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void decrement() {
    _counter--;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void reset() {
    _counter = 0;
    notifyListeners();
  }
}

Provider로 만들어 본 Counter APP 예제

Provider.of 메소드에는 listen 이라는 속성이 존재한다. 이것의 의미는 아래와 같다.

Provider.of<Counter>(context, listen: false)

위의 코드에서 listen 속성은 false이다. 이것의 의미는 Counter 모델에 변화가 생겨도 UI를 다시 그리지 않겠다는 의미이다.

MyHomePage 클래스를 ChangeNotifierProvider로 감싸고 있다. create 속성에서 초기값을 정하고 이를 자식 위젯들에서 사용한다.

ChangeNotifierProvider를 이용하면 StatefulWidget을 사용하지 않아도 화면이 갱신되는 것을 확인할 수 있다. 그리고 실제 변경이 이루어지는 것은 Counter클래스의 counter 변수이다. 그렇기 때문에 listen 파라미터는 변경되는 곳에서만 true로 설정하면 된다.

위의 코드에서는 Counter클래스의 counter 변수 값을 가져올 때 Consumer를 사용했다. Consumer는 listen 파라미터가 없다. 만약, Provider를 직접적으로 사용하려면 아래와 같이 사용하면 된다.

Provider.of<Counter>(context, listen: true).counter.toString(),

즉, Consumer를 사용하면 해당 위젯을 감싸주어야하고, 그것이 아니라 Provider를 직접적으로 사용하려면 위와 같이 사용하면 된다.


다른 페이지에서도 Provider 객체 사용하기

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ChangeNotifierProvider 위젯으로 감싸준다.
    return ChangeNotifierProvider<Counter>(
      create: (context) => Counter(0),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          // primarySwatch: Colors.blue,
          brightness: Brightness.dark,
          primaryColor: Colors.lightBlue[800],
          accentColor: Colors.cyan[600],
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider 테스트'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              'Count Number',
              style: Theme.of(context).textTheme.headline4,
            ),
            Text(
              Provider.of<Counter>(context, listen: true).counter.toString(),
              style: Theme.of(context).textTheme.headline2,
            ),
            SizedBox(
              height: 20,
            ),
            RaisedButton(
              child: Text('다음 페이지'),
              onPressed: () {
                gotoSecondPage(context);
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Stack(
        children: <Widget>[
          Positioned(
            right: 10,
            bottom: 120,
            child: FloatingActionButton(
              heroTag: 'btn1',
              child: Icon(Icons.add),
              onPressed: () {
                // setState() 메소드를 호출하지 않아도 화면이 업데이트된다.
                // 이 말인즉슨, stateless 클래스에서도 화면 갱신을 할 수 있다는 뜻이다.
                // listen 매개변수는 플러터 프레임워크에 변경된 것을 알릴지 설정하는
                // 매개변수이다. 헌데, 실제 변경되는것은 Counter.counter 변수이므로
                // listen을 false로 주었다.
                Provider.of<Counter>(context, listen: false).increment();
              },
            ),
          ),
          Positioned(
            right: 10,
            bottom: 40,
            child: FloatingActionButton(
              heroTag: 'btn2',
              child: Icon(Icons.horizontal_split),
              onPressed: () {
                Provider.of<Counter>(context, listen: false).decrement();
              },
            ),
          ),
          Positioned(
            left: 40,
            bottom: 40,
            child: FloatingActionButton(
              heroTag: 'btn3',
              backgroundColor: Colors.red,
              child: Icon(Icons.repeat),
              onPressed: () {
                Provider.of<Counter>(context, listen: false).reset();
              },
            ),
          ),
        ],
      ),
    );
  }

  Future gotoSecondPage(BuildContext context) {
    return Navigator.push(
        context, MaterialPageRoute(builder: (context) => Page2()));
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('page2'),
      ),
      body: Center(
        child: Container(
          child: Text(
            Provider.of<Counter>(context).counter.toString(),
            style: Theme.of(context).textTheme.headline3,
          ),
        ),
      ),
    );
  }
}

class Counter with ChangeNotifier {
  int _counter = 0;

  Counter(this._counter);

  // _counter 변수 값을 가져올 counter getter 메소드
  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void decrement() {
    _counter--;
    notifyListeners(); // 값이 변할 때마다 플러터 프레임워크에 알려줌.
  }

  void reset() {
    _counter = 0;
    notifyListeners();
  }
}

다른 페이지에서 Provider 값 가져오는 예제

위의 예제는 다른 페이지에서도 Provider 객체를 사용하는 예제이다. 그러므로 ChangeNotifierProvider를 기존 MyHomePage 감싸주는 것을 더 상위 객체인 MaterialApp으로 옮겼다. 이렇게하지 않으면 당연히 에러가 날 것이다. 왜냐면 MyHomePage클래스와 Page2클래스는 위젯트리에서 동등한 레벨에 존재하기 때문이다. 그리하여 MyHomePage에서 Provider를 감싸주면 Page2에서는 참조할 수 없다. 그 이상의 존재를 Provider로 감싸주어야 두 클래스 모두 참조할 수 있는 객체가된다.

이렇게 Provider를 쓰는 것은 간단하다. 허나 Provider가 여러개 일 때 관리가 어렵고 중첩될 경우 코드가 난잡해질 수 있다. 이럴 때는 MultiProvider를 사용해야 한다.


MultiProvider : 여러 프로바이더 사용

MultiProvider를 사용하면 Provider 관리가 상당히 편해진다. 다른 위젯들과도 섞일 일이 없어 사용하기 편하다.

사용법은 간단하다. MultiProvider의 providers 속성에 원하는 Provider를 선언해주면 된다. 다음은 MultiProvider의 사용 예제코드이다.

Widget build(BuildContext context) {
  return MultiProvider(
    providers: <Provider>[
      Provider<int>.value(value: 55),
      Provider<String>.value(value: 'Hello World!'),
    ],
  );
}

Provider는 자료형으로 어떤 값을 가져올지 구분한다. 그런데 MultiProvider에 같은 자료형이 여러번 정의되어 있다면 어떤 것을 가져올까?

MultiProvider(
  providers: <Provider>[
    Provider<int>.value(value: 5),
    Provider<int>.value(value: 10),
    Provider<int>.value(value: 15),  // Provider.of<int>로 접근시 이 값만 가져옴
  ]
)

MultiProvider는 같은 자료형이 여러개 존재할 때 가장 밑에 있는 Provider를 가져온다. 즉, 15를 가져온다.


www.youtube.com/watch?v=vFxk_KJCqgk

Comments