[Flutter]Provider를 이용한 상태관리

한상욱·2023년 11월 20일
1

Flutter

목록 보기
5/26
post-thumbnail

들어가며

여기에 3개의 UI가 있다고 하겠습니다. 이 UI들은 각각 동일한 데이터를 공유해야 된다고 하겠습니다. 그렇다면 우리는 해당 위젯을 아래처럼 아규먼트를 넘겨주면서 데이터를 전달할 수 있을 것입니다.

이렇게 되면 한가지 문제점이 나타날 수 있습니다. 만약 서로 공유하고 있는 아규먼트 데이터가 세번째 UI에서 변경되었을 때, 이를 공유하고 있는 UI는 리빌드 되야 해당 데이터의 변경을 적용할 수 있습니다. 우리는 이 문제를 어떻게 해결할 수 있을까요?

이를 해결하기위하여 상태관리라는 개념이 만들어졌으며, 맨 처음 Flutter로 상태관리를 입문하는 것이 Provider 입니다.

Provider를 사용하는 이유

Provider는 GetX, BloC과 같은 상태관리 솔루션 라이브러리입니다. 현재 해당 라이브러리는 Riverpod를 만들게 된 계기가 되었지만 처음 나왔을때는 굉장히 획기적인 상태관리 솔루션이었습니다. 이제, Provider를 이용하여 상태관리를 해볼까요?

아래와 같은 예제앱이 있습니다.

import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter.dart';

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

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Counter(),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter_controll.dart';

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

  
  Widget build(BuildContext context) {
    int count = 0;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "$count",
              style: const TextStyle(fontSize: 40),
            ),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) => CounterControll(count: count)));
                },
                child: const Text("카운터 상세"))
          ],
        ),
      ),
    );
  }
}
// ignore_for_file: must_be_immutable

import 'package:flutter/material.dart';

class CounterControll extends StatefulWidget {
  int count;
  CounterControll({super.key, required this.count});

  
  State<CounterControll> createState() => _CounterControllState();
}

class _CounterControllState extends State<CounterControll> {
  void increase() => setState(() {
        widget.count++;
      });

  void decrease() => setState(() {
        widget.count--;
      });

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text(
            "${widget.count}",
            style: const TextStyle(fontSize: 40),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              ElevatedButton(onPressed: increase, child: const Icon(Icons.add)),
              ElevatedButton(
                  onPressed: decrease, child: const Icon(Icons.remove)),
            ],
          )
        ],
      ),
    );
  }
}

해당 예제는 초기 UI에서 나타난 카운터를 그 다음 페이지가 이어받아서 해당 카운터를 증가 감소 시킬 수 있는 앱입니다.

하지만 보다시피 해당 앱을 통해 두번째 UI에서만 숫자가 변할 뿐 이전 화면에서는 전혀 숫자가 변하지 않습니다. 게다가 대규모 앱에서는 동일한 데이터를 더 많은 화면에서 접근하려 하겠죠. 이를 해결하기 위해서 Provider를 이용할 수 있습니다.

Provider 사용하기

Provider를 사용하기 위해서는 해당 라이브러리를 설치해야 합니다.

$ flutter pub add provider

이제 가장 중요한 CounterModel을 생성하겠습니다. 우리는 이 Model을 통해서 모든 화면에서 접근하는 counter에 대한 값을 처리할 것입니다.

import 'package:flutter/material.dart';

class CounterModel with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increase() {
    _count++;
    notifyListeners();
  }

  void decrease() {
    _count--;
    notifyListeners();
  }
}

가장 중요한 것은 ChangeNotifier를 mixin 하는 것입니다. notifyListeners()를 통해서 해당 Provider를 참조하는 모든 위젯에 정보를 뿌려줄 수 있습니다. 이제 MaterialApp에 Provider를 적용해야합니다.

import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';

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

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

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const MaterialApp(home: Counter()),
    );
  }
}

하나의 위젯이 아니라 앱 전체에서 사용하기 위해서는 ChangeNotifierProvider를 이용해서 MaterialApp을 감싸주어야 합니다. 그렇지 않은 경우에는 해당하는 child에만 Provider를 감싸줍니다.

import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter_controll.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Consumer<CounterModel>(builder: (context, counter, child) {
              return Text(
                "${counter.count}",
                style: const TextStyle(fontSize: 40),
              );
            }),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) => const CounterControll()));
                },
                child: const Text("카운터 상세"))
          ],
        ),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Consumer<CounterModel>(builder: (context, counter, child) {
            return Text(
              "${counter.count}",
              style: const TextStyle(fontSize: 40),
            );
          }),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              ElevatedButton(
                  onPressed: context.read<CounterModel>().increase,
                  child: const Icon(Icons.add)),
              ElevatedButton(
                  onPressed: context.read<CounterModel>().decrease,
                  child: const Icon(Icons.remove)),
            ],
          )
        ],
      ),
    );
  }
}

이제 Provider에 접근하는 방법에 대해서 알아보겠습니다. 위에처럼 Consumer를 통해서 접근할 수도 있으나, Provider.of(context)를 통해서 접근하는 것이 더 간단할 수도 있습니다. 이제 앱 전역에서 Provider로 선언한 Counter 변수에 접근할 수 있습니다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글