티스토리 뷰

screen_item.dartProvider는 앞서 다룬 Bloc과 비슷하게 단순히 앱 전역에 상태를 공유한다는 것 말고도 관심사를 분리할 수 있다는 특징이 있다.

 

기본적으로 플러터는 UI와 기능이 함께 작성되기 때문에 한 파일 내의 코드 양이 많아질 수 있으며, UI와 기능을 분리하기 어려워 수정 등이 불편하다.

 

Provider를 사용하면 Provider의 주요 개념에 따라 코드를 작성하게 되고, 자연스럽게 UI와 상태를 관리하는 기능 영역을 분리할 수 있다.

 

바로 한번 들어가보자.


Provider 패키지의 구성 요소는 기본적이로 상태(데이터)를 생산 / 소비하는 영역으로 나누게 된다. 

 

상태를 생산하는 영역을 Provider, 소비하는 영역을 Consumer라고 한다. 

 

상태를 생산한다는 것은 상태를 만들고 변화시키는 등 상태를 관리한다는 의미이며, 소비한다는 것은 상태를 화면에 표시하거나 하는 등 상태를 사용한다는 뜻이다. 

 

아주 간단한 Counter 예제로 한번 이 개념들을 확인해보자.

 

1. Provider

◎provider 

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

먼저 상태를 생산하는 Provider는 상태의 타입과 초기값을 위 코드처럼 선언할 수 있으며, child에 위젯을 넣어 해당 위젯 내부에서 상태를 사용할 수 있도록 한다.

 

즉, 모든 위젯에서 접근하도록 하려면 main.dart의 build 메서드에서 Provider를 리턴하도록 하면 된다.

 

- Consumer

◎Consumer

// Consumer 상태 소비 1
return Consumer<int> {
	builder: (context, data, child) {
    	return Text(data.toString());
    }
}

// Consumer 상태 소비 2
int data = Provider.of<int>(context);

Consumer는 앞서 Provider에서 설명한 것처럼 Provider가 감싸는 범위 내에서 작성할 수 있다. 가장 꼭대기에 Provider가 있고, 그 하위에서 Consumer를 작성해 값을 받아온다고 생각할 수 있다. 

 

1. Consumer Widget: Consumer 위젯은 builder를 통해 상태 값을 받아와 위젯을 빌드한다. 이렇게 builder 안에 선언된 위젯은 상태 값이 변화할 때마다 다시 빌드되어, 가급적 상태가 필요한 부분만 builder 안에 선언하여 불필요하게 많은 부분이 매번 빌드되지 않도록 해야한다.

 

2. Provider.of: Consumer 위젯보다 조금 더 간단하게 Provider.of(context)를 사용하여 상태 값을 받아올 수도 있다.


위에 작성한 Provider는 그저 어떤 값을 전달해주기 위한, 말 그대로 제공자의 역할을 하고 있을 뿐 실제로 상태의 변화를 다룰 수는 없다. 

 

Provider가 상태의 변화를 다루기 위해서는 ChangeNofifer 와 ChangeNotiferProvider 를 사용해야 하는데, 이제 그 개념에 대하여 알아보도록 하자.

 

- ChangeNofifer

ChangeNofifer는 단어에서 알 수 있듯, 변화를 알려주는 역할의 객체이다. 따라서 어떤 객체를 ChangeNofifer를 활용해 만들면 해당 객체 값의 변화를 감지하여 알 수 있는데, 이것을 구독이라고 표현한다.

 

예시를 위해 count 상태 관리를 위한 Counter 클래스를 한번 보자.

 

◎counter.dart

class Counter {
  int count;
  Counter(this.count);

  void increment() {
    count += 1;
  }

  void decrement() {
    count -= 1;
  }
}

위 코드처럼 작성하면 Counter 의 count 값의 변화를 실시간으로 받아올 방법이 없다. 그러면 UI도 해당 변수를 빌드하지 못한다.

 

이를 실시간 변화 감지가 가능하도록 ChangeNotifier를 사용한다.

 

◎counter.dart

class Counter extends ChangeNotifier{
  int count;
  Counter(this.count);

  void increment() {
    count += 1;
    notifyListeners();
  }

  void decrement() {
    count -= 1;
    notifyListeners();
  }
}

ChnageNotifier를 Counter 클래스에 상속하면 위처럼 변화를 구독할 수 있는 객체가 되며 notifyListeners() 를 사용할 수 있데 된다. notifyListeners()를 통해 Counter 의 값이 변화했음을 알려주게 된다.

 

- ChangeNotiferProvider

이렇게 작성된 상태를 생산할 수 있는 위젯이 바로 ChangeNofifierProvider이며, Provider에 ChnageNotifier가 추가된 형태라고 할 수 있다. 

 

◎main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(...);
    );
  }
}

 ChangeNofifierProvider 는 단일 상태만 다룰 수 있기 때문에, 2개 이상의 상태를 전역에서 다루는 경우에는 child 하위에 Provider를 하나 더 생성해서 여러 개의 상태를 다루도록 할 수 도 있지만 이런 방식보다는 provider 패키지에서 여러 개의 Provider를 함께 사용할 수 있는 MultiProvider 또한 지원한다.

 

◎main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => State1()),
        Provider(create: (context) => State2())
      ],
    );
  }
}

이제 Provider의 기본적인 구조를 다뤄봤으니, 아주 간단한 프로젝트를 만들어서 Provider를 어떻게 적용하여 사용하는 지 한번 알아보자.

 

Provider의 적용 예시로 선택한 상품을 장바구니에 담는 기능을 구현해보려고 한다. 

 

프로젝트는 다음과 같이 구성한다.


 > models 

   - item.dart: item 모델, 단순 모델로 값 변화 없음

   - cart.dart: cart 모델, 장바구니를 구현하기 위한 것으로 상품을 담고 빼는 이벤트 있음, 카트 내 상품을 List<Item>으로 관리해 값이 변화할 수 있음

 

> repositories

  - item_list.dart: 상품 더미데이터

 

> screens

  - screen_item.dart: 상품 목록 조회 화면

  - screen_cart.dart: 장바구니 조회 화면


◎item.dart

import 'package:flutter/cupertino.dart';

class Item {
  final int id;
  final String title;
  final int price;
  Item({required this.id, required this.title, required this.price});
}

 

◎cart.dart

import 'package:flutter/material.dart';
import 'package:flutter_state_provider/models/item.dart';

class Cart extends ChangeNotifier {
  final List<Item> items = [];

  int getTotalPrice() {
    int total = 0;
    for (Item item in items) {
      total += item.price;
    }
    return total;
  }

  void addItem(Item item) {
    items.add(item);
    notifyListeners();
  }

  void deleteItem(int id) {
    items.removeWhere((item) => item.id == id);
    notifyListeners();
  }
}

cart.dart는 Cart의 값 변화가 발생해야 하므로, ChangeNitifier를 통해 값의 변화를 감지할 수 있어야 한다.

 

코드는 Item 객체를 추가/삭제하고 Item List의 총 가격을 리턴하는 메서드로 구성한다.

 

보면,  선언부에 있는 items 라는 리스트의 상태를 변화시키는 메서드만 notifyLiseners() 를 가지고 있다.

 

◎item_list.dart

import 'package:flutter_state_provider/models/item.dart';

class ItemList {
  final List<Item> items = [
    Item(id: 1, title: '1번 상품', price: 1000),
    Item(id: 2, title: '2번 상품', price: 1500),
    Item(id: 3, title: '3번 상품', price: 2000),
    Item(id: 4, title: '4번 상품', price: 3000),
    Item(id: 5, title: '5번 상품', price: 4000),
    Item(id: 6, title: '6번 상품', price: 5000),
  ];

  List<Item> getItems() {
    return items;
  }
}

item_list.dart는 그저 상품의 더미 데이터 제공을 위해 생성한 Class이다. 

 

◎main.dart

import 'package:flutter/material.dart';
import 'package:flutter_state_provider/models/cart.dart';
import 'package:flutter_state_provider/repositories/item_list.dart';
import 'package:flutter_state_provider/screens/screen_cart.dart';
import 'package:flutter_state_provider/screens/screen_item.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Cart()),
        Provider(create: (_) => ItemList()),
      ],
      builder: (context, child) {
        return MaterialApp(
          title: 'Flutter Provider Example',
          initialRoute: '/',
          routes: {
            '/': (context) => ItemScreen(),
            '/cart': (context) => CartScreen(),
          },
        );
      },
    );
  }
}

Provider는 하위 위젯에 상태를 제공할 수 있어야 하므로, 하위 위젯을 감쌀 수 있는 충분한 상위 위젯에서 선언되는 것이 바람직하기 때문에 main.dart에서 선언을 했다.  

 

Provider 또는 ChangeNotifierProvider를 선언할 때는 어떤 클래스를 제공할 것인지 위와 같이 작성해야 한다.

 

또한 Provider는 단순히 상태를 다루는 클래스만 제공해주는 것이 아닌,  ItemList와 같이 정적인 데이터를 제공해주는 객체도 부모 위젯에서 선언만 해준다면 하위 위젯에서 사용할 때마다 매번  ItemList 객체를 새롭게 만들지 않아도 되기 때문에 좀 더 메모리 관리가 쉬워진다. 

 

 

◎screen_item.dart

import 'package:flutter/material.dart';
import 'package:flutter_state_provider/models/cart.dart';
import 'package:provider/provider.dart';
import 'package:flutter_state_provider/models/item.dart';
import 'package:flutter_state_provider/repositories/item_list.dart';

class ItemScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<Cart>(context);
    final itemList = Provider.of<ItemList>(context);

    List<Item> items = itemList.getItems();

    return Scaffold(
      appBar: AppBar(
        title: Text('상품 목록'),
        actions: [
          IconButton(
            icon: Icon(Icons.shopping_cart),
            onPressed: () {
              Navigator.of(context).pushNamed('/cart');
            },
          )
        ],
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return Builder(
            builder: (context) {
              bool isInCart = cart.items.contains(items[index]);
              return ListTile(
                title: Text(items[index].title),
                subtitle: Text(items[index].price.toString()),
                trailing: isInCart
                    ? Icon(Icons.check)
                    : IconButton(
                        icon: Icon(Icons.add),
                        onPressed: () {
                          cart.addItem(items[index]);
                        },
                      ),
              );
            },
          );
        },
      ),
    );
  }
}

상위 위젯인 MaterialApp 에서 제공하는 ItemList를 받아와 화면에 ListView로 나타낼 ItemScreen 클래스이다.

 

build 메서드 내에

final cart = Provider.of<Cart>(context);
final itemList = Provider.of<ItemList>(context);

로 알 수 있듯, Provider.of(context) 를 사용하여 상위 위젯에서 제공한 객체를 그대로 가져와서 사용할 수 있다.

 

이외에는 특별한 내용이 없으니 넘어가자.

 

 

◎screen_cart.dart

import 'package:flutter/material.dart';
import 'package:flutter_state_provider/models/cart.dart';
import 'package:provider/provider.dart';

class CartScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = Provider.of<Cart>(context);
    final items = cart.items;
    return Scaffold(
      appBar: AppBar(
        title: Text('내 카트'),
        actions: [
          Center(
            child: Container(
              padding: EdgeInsets.all(10),
              child: Text(
                '총액 : ' + cart.getTotalPrice().toString(),
                style: TextStyle(fontSize: 15),
              ),
            ),
          )
        ],
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return Builder(
            builder: (context) {
              return ListTile(
                title: Text(items[index].title),
                subtitle: Text(items[index].price.toString()),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () {
                    cart.deleteItem(items[index].id);
                  },
                ),
              );
            },
          );
        },
      ),
    );
  }
}

CartScreen도 마찬가지로 ItemScreen에서 추가한 ItemList를 그대로 상위 위젯에서 가져와서 띄워주는 간단한 기능만이 있다.


App도 아주 잘 동작한다.

 


마무리로 Provider 방식을 요약하면 다음과 같다.

 

1. Provider: 말 그대로 "제공"해주는 역할, 상위 위젯에서 선언한 객체를 그대로 하위 위젯에서 사용할 수 있도록 제공한다.

 

2. Consumer: 하위 위젯에서 상위 위젯의 객체를 받아와 "사용" 하는 역할, Consumer 위젯보다는 Provider.of(context)의 형태로 많이 사용된다.

 

3. ChangeNotifier: 특정 클래스가 변화할 수 있는 값을 포함할 때 상속받아야 하는 클래스, 값이 변화했음을 notifyListeners()로 알려줄 수 있다.

 

4. ChangeNotifierProvider: ChangeNotifier를 상속할 클래스로 만들어진 객체를 "제공"해주는 Provider이다.


 

끝!!

 

'Mobile > Flutter' 카테고리의 다른 글

[Flutter] 전역 상태 관리 기법: Bloc  (0) 2022.07.25
[Flutter] API 연동  (0) 2022.07.24
[Flutter] State  (0) 2022.07.11
[Flutter] 화면 전환  (0) 2022.07.09
[Flutter] Buttons  (0) 2022.07.09
Comments