왜 상태관리 라이브러리를 써야 하는가

abstract class State<T extends StatefulWidget> with Diagnosticable {
  // ...
  @protected
  void setState(VoidCallback fn) {
    // ...
    _element!.markNeedsBuild();
  }
} // State

플러터 위젯 상태(state)를 관리하는 가장 기초적인 방법은 setState인데, 이는 state를 업데이트 하고 위젯을 다시 빌드하는 메소드입니다. sdk를 보면 내부는 사실 markNeedsBuild()만 호출하는게 거의 전부입니다. 

setState의 단점은 위젯 트리의 뎁스가 커지면 답이 없습니다. 부모 자식 위젯간에 콜백으로 연결돼있어서 UI에 로직이 강하게 결합된 스파게티 코드가 만들어집니다.

하위트리 어디든 상태값 구독이 가능한 InheritedWidget이 있긴 하지만 상태 변경이 안되는 단점이 있습니다. ScopedModel은 minimal rebuild가 안됩니다. 결국은 라이브러리의 힘을 빌려야 합니다.

 

많이 사용하는 상태 관리 라이브러리

  • bloc: BLoC(Business Logic Component) 패턴. Dartcon 2018에서 구글 개발자가 발표 (링크)
    싱크, 스트림이라는 개념을 활용
  • provider: BLoC의 러닝커브를 보완 + Dependency Injection (InheritedWidget의 wrapper)
    Flutter favorite에 선정
    • riverpod: (provider의 애너그램) provider에서 플러터를 걷어낸 순수 dart 라이브러리. 컴파일타임 safe 등 여러 장점도 포함
  • (getx): 간단하고 사용이 편리 BUT 빌드컨텍스트를 사용하지 않아 논란의 대상

큰 규모 앱은 보통 bloc + provider(riverpod) 조합을 사용한다고 합니다. 즉 상태관리 라이브러리도 여러가지를 조합해서 쓸 수 있나 봅니다. 단, getx는 컨텍스트를 사용하지 않기 때문에 섞어 쓰면 안되지 않나..? 라는 개인적인 생각입니다.

이중에 provider로 만든 간단한 카운터 예제를 만들어보겠습니다.

 

provider 카운터 예제

초 심플한 예제입니다. + 버튼을 누르면 숫자가 1 늘어나고, - 버튼을 누르면 숫자가 1 줄어듭니다.

Provider에서는 세가지 구성요소를 알아야 합니다

  • ChangeNotifier
    • 값이 변경되면 리스너에게 notify 할 수 있는 클래스
  • ChangeNotifierProvider
    • 하위 위젯에 "ChangeNotifier"를 제공해주는 클래스
  • Consumer
    • provider의 값을 받아서 실제로 사용하는 부분
    • 딱 빌드가 되어야 할 부분에만 감싸야 비효율적인 리빌딩을 막을 수 있음
lib
├── main.dart
├── provider
│   └── count_provider.dart
└── screen
    └── home.dart

구조는 이런식으로 했는데 파일 하나하나씩 살펴봅시다

참고로 'flutter pub add provider'로 pubspec.yaml에 라이브러리 추가해줘야 합니다

import 'package:flutter/material.dart';

class CountProvider extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

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

count_provider.dart

ChangeNotifier를 상속받은 클래스를 만들어줍니다. 버튼을 눌렀을 때의 액션에 대한 메소드도 각각 만들어줍니다.

notifyListeners() 함수를 꼭 호출해야 하는점 주의하세요

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

import 'provider/count_provider.dart';
import 'screen/home.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: ChangeNotifierProvider(
        create: (_) => CountProvider(),
        child: Home(),
      ),
    );
  }
}

main.dart

원래 home: Home() 으로 되어 있을 부분이 ChangeNotifierProvider로 감싸져 있습니다. create에서 CountProvider를 생성해 Home 위젯에서 사용할 수 있습니다.

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

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

  late CountProvider _countProvider;

  @override
  Widget build(BuildContext context) {
    _countProvider = Provider.of<CountProvider>(context, listen: false);

    return Scaffold(
      appBar: AppBar(
        title: Text('provider sample'),
      ),
      body: CountHome(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            onPressed: () => _countProvider.increase(),
            icon: Icon(Icons.add),
          ),
          IconButton(
            onPressed: () => _countProvider.decrease(),
            icon: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<CountProvider>(
        builder: (context, countProvider, child) => Text(
          Provider.of<CountProvider>(context).count.toString(),
          style: TextStyle(fontSize: 60),
        ),
      ),
    );
  }
}

home.dart

여기서 provider의 메소드를 호출해 상태를 변경하고, Consumer를 이용해 상태가 변경될 때마다 빌드되도록 만들었습니다.

provider를 갖고올 때 listen 속성이 false인데 그 이유는 Home에서는 상태 변경만 할 뿐 해당 위젯을 다시 빌드할 필요는 없기 때문입니다. 대신 숫자 부분만 다시 빌드하면 되니 CountHome만 Consumer로 감싸주었습니다.

이렇게 간단한 카운터 예제를 만들어봤는데요, 상태관리는 클라 개발에 핵심적인 요소이니 많은 연습이 필요하겠습니다.

 

Reference

반응형
  1. 동건 2022.05.30 07:31 댓글주소 수정/삭제 댓글쓰기

    컴파일시 아래와 같은 에러가 생기는 이유가 뭔지
    알려주시면 감사하겠습니다

    Launching lib\main.dart on AOSP on IA Emulator in debug mode...
    lib\main.dart:1
    /C:/src/flutter/.pub-cache/hosted/pub.dartlang.org/provider-6.0.3/lib/src/selector.dart:75:29: Error: The method 'selector' isn't defined for the class 'Selector0 Function()'.

    'Selector0' is from 'package:provider/src/selector.dart' ('/C:/src/flutter/.pub-cache/hosted/pub.dartlang.org/provider-6.0.3/lib/src/selector.dart').
    package:provider/src/selector.dart:1
    Try correcting the name to the name of an existing method, or defining a method named 'selector'.
    final selected = widget.selector(context);