flutter의 다양한 상태관리 라이브러리

김연후·2022년 8월 11일
0

출처
[Flutter Festival GDG Songdo] GetX, provider, bloc 패턴 비교 분석 - 유병욱

요약

  • 간단한 Counter를 구현하여 총 5가지 코드 비교
    • Plain(Stateful Widget)
    • Bloc (with Cubit)
    • Provider
    • GetX
    • riverpod

Plain(Stateful Widget)

count state를 Stateful Widget 내에 선언

// stateful widget
class CountPage extends StatefulWidget {
  const CountPage({Key? key}) : super(key: key);

  @override
  _CountPageState createState() => _CountPageState();
}

// state of the widget
class _CountPageState extends State<CountPage> {
  int _count = 0; // state 선언

  @override
  Widget build(BuildContext context) {
  ...

counter의 +, - 버튼에 setState로 state 업데이트

children: [
  FloatingActionButton(
    onPressed: () => setState(() {
      _count++;
    }),
    child: const Icon(Icons.add),
  ),
  const SizedBox(height: 8),
  FloatingActionButton(
    onPressed: () => setState(() {
      _count--;
    }),
    child: const Icon(Icons.remove),
  ),
],

setState 코드

@protected
  void setState(VoidCallback fn) {
  
  	...
    
  	_element!.markNeedsBuild();
  }

markNeedsBuild 메서드는 flutter에 다음 프레임에 UI를 re-render하도록 알린다.
markNeedsBuild 메서드를 보자.

/// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame.
///
/// Since it is inefficient to build an element twice in one frame,
/// applications and widgets should be structured so as to only mark
/// widgets dirty during event handlers before the frame begins, not during
/// the build itself.
void markNeedsBuild() {
	
    ...
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
}
  • dirty 값을 true로 두어 element에 마킹을 하여 다음 프레임에 rebuild할 전역 widget 리스트에 추가해둠

setState()를 이용하여
1. state를 업데이트 하고,
2. flutter에게 UI를 re-render하도록 알림

언제쓸까?

  • 특정한 Widget 내에서 단기적으로 쓰이고 말 때, 사용하면 편한 상태관리
    (한 파일 내에서 쓰기에 편한 상태관리 방식)

Bloc

Stream을 Flutter Project 내에 적극적으로 사용해야한다면 사용해야하는 라이브러리!

Cubit과 Bloc

Cubit은 function으로 state를 관리 할 수 있게해주는 class

  • 단순 function 호출로 state 변경
    Bloc은 event 발생으로 state를 관리 할 수 있게해주는 class
  • event를 감지해 state 변경
    Cubit
    Bloc

Cubit 생성

import 'package:flutter_bloc/flutter_bloc.dart';

class CountCubit extends Cubit<int> {

  CountCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}
  • Cubit을 생성할 때, type(위의 경우 int) 지정 필요, 원시 타입 외에 사용자 지정 class를 생성해서 사용할 수도 있다.
  • emit함수는 Cubit 클래스 내에서만 사용 가능하며, 새로운 state 값을 지정해준다.

Cubit 사용

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CountCubit(),
      child: const CountView(),
    );
  }
}
  • CountCubit을 CountPage에서 사용하기 위해 BlocProvider의 create 함수에 지정

BlocProvider

  • Flutter widget
  • create함수로 새로운 Bloc을 생성
  • 의존성 주입(DI)으로 하위 widget(child)들에게 생성한 하나의 Bloc(or Cubit) 인스턴스 전달
  • 따라서 CountView widget에서 CountCubit 사용 가능

BlocBuilder

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('State Manager - BloC'),
      ),
      body: Center(
        child: BlocBuilder<CountCubit, int>(
          builder: (BuildContext context, int state) {
            return Text('$state', style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold));
          },
        ),
      ),
	...
}
  • Flutter widget으로 bloc과 builder로 구성됨 (StreamBuilder와 유사)
  • builder 함수에서 state에 따라 widget을 반환

context.read

floatingActionButton: Column(
  mainAxisAlignment: MainAxisAlignment.end,
  crossAxisAlignment: CrossAxisAlignment.end,
  children: [
    FloatingActionButton(
      onPressed: () => context.read<CountCubit>().increment(),
      child: const Icon(Icons.add),
    ),
    const SizedBox(height: 8),
    FloatingActionButton(
      onPressed: () => context.read<CountCubit>().decrement(),
      child: const Icon(Icons.remove),
    ),
  ],
),
  • CountCubit 클래스 형식의 가장 근접한 조상의 인스턴스를 가리킴
  • 여기서는 BlocProvider에서 생성한 CountCubit 클래스로 만들어진 인스턴스
  • increment(), decrement() 함수로 state값을 변경
    • Cubit이므로 state를 단순 함수로 변경

BLoc 사용
추후 flutter로 로그인 기능 구현에서...

Cubit

  • state 변경을 function으로 관리
  • 변경 된 값을 가져올 때, emit / onChanged 등으로 개발자가 시점을 특정할 수 있음
  • 단순 UI 핸들링에 효과적

Bloc

  • state 변경을 event로 관리
  • 여러 요인으로 인한 잦은 변경이 많은 관리
  • 주체에 대해 event로 관리되기에 변화에 대한 추적 가능
  • 로그인 유무 / 특정 Action에 대한 추적을 Stream으로 관리하기에 효과적

언제쓸까?

  • Stream을 적극적으로 활용해야될 때 (event기반의 Bloc, function기반의 Cubit)
  • View와 비즈니스Logic에 대한 분명한 분리

Provider

state 관리 그 자체만을 위하여...

관리할 state model 생성

class CountModel extends ChangeNotifier {
  int counter = 0;

  void incrementCounter() {
    counter++;
    notifyListeners();
  }

  void decrementCount() {
    counter--;
    notifyListeners();
  }
}

ChangeNotifier class

  • 변경 알림(change notification) 기능을 제공하는 클래스
  • notifyListeners() 함수가 counter state가 변경되었음을 ChangeNotifierProvider에 알림

Provider 사용하기

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'State Manager - Provider',
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider<CountModel>(
            create: (_) => CountModel(),
            child: const CountPage(),
          ),
        ],
      ),
    );
  }
}

Root Widget에서 provider 사용 (하위(자식) Widget에서 state관리 가능)

  • 하나의 Provider만 사용하고자 하면 home에 ChangeNotifierProvider를 바로 작성해도 된다.

ChangeNotifierProvider

  • ChangeNotifier의 인스턴스를 하위 Widget들에게 제공 (CountModel)
  • ChangeNotifierProvider가 widget트리에서 제거되면 이전에 생성한 ChangeNotifier 인스턴스도 자동으로 삭제(dispose)

Consumer와 Provider.of(context)
CountModel이 ChangeNotifierProvider에 의해 하위 Widget들에게 제공되는 상황에서 이를 사용하기 위한 두 가지의 방식이 있다.

  • Consumer
@override
Widget build(BuildContext context) {
  return Consumer<CountModel>(
    builder: (context, model, child) {
      return Center(
        child: Text(
          '${model.counter}',
          style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
        ),
      );
    },
  );
}
  • Provider.of(context)
@override
Widget build(BuildContext context) {
  final countModel = Provider.of<CountModel>(context);
  return Center(
    child: Text(
      '${countModel.counter}',
      style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
    ),
  );
}
  • Consumer와 Provider.of(context)의 성능은 동일
  • Provider.of(context)의 경우 사용하는 Widget 전체가 rebuild 된다.
    • 따라서 Widget을 잘게 쪼개서 rebuild하는 코드를 줄이거나
    • Consumer를 사용하여 일부분만 rebuild 할 수 있다.
return Consumer<CountModel>(
  builder: (context, model, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text('Counter: ${model.counter}'),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

builder 함수의 child argument에 할당한 SomeExpensiveWidget()은 CountModel의 state가 바뀌어도 rebuild하지 않는다.

위와 같은 경우는 큰 Widget의 부모로 Consumer Widget이 필수적으로 있어야 하는 경우 사용할 수 있는 방법이지만, 굳이 부모에 있을 필요가 없다면 최대한 하위 Widget으로 Consumer를 배치하는 것이 성능적으로 유리하다. flutter docs

// DON'T DO THIS
return Consumer<CountModel>(
  builder: (context, model, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Counter: ${model.counter}'),
      ),
    );
  },
);
// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CountModel>(
      builder: (context, model, child) {
        return Text('Counter: ${model.counter}');
      },
    ),
  ),
);

Consumer와 Provider.of(context) 둘 다 함수를 이용하여 state 변경 가능

return Scaffold(
  appBar: AppBar(
    title: const Text('State Manager - Provider'),
  ),
  body: const CountView(),
  floatingActionButton: Consumer<CountModel>(
    builder: (context, model, child) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => model.incrementCounter(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => model.decrementCount(),
            child: const Icon(Icons.remove),
          ),
        ],
      );
    },
  ),
);
final countModel = Provider.of<CountModel>(context);
return Scaffold(
  appBar: AppBar(
    title: const Text('State Manager - Provider'),
  ),
  body: const CountView(),
  floatingActionButton: Column(
    mainAxisAlignment: MainAxisAlignment.end,
    crossAxisAlignment: CrossAxisAlignment.end,
    children: [
      FloatingActionButton(
        onPressed: () => countModel.incrementCounter(),
        child: const Icon(Icons.add),
      ),
      const SizedBox(height: 8),
      FloatingActionButton(
        onPressed: () => countModel.decrementCount(),
        child: const Icon(Icons.remove),
      ),
    ],
  ),
);

Provider

  • InheritedWidget을 좀 더 쓰기 편한 형태로 발전시킨 상태관리 모델
  • Flutter에서 적극 추천하는 상태관리 모델
  • Provider는 Flutter 기반을 최대한 유지 및 활용하는 형태로 구현 가능 (Provider 객체를 가지고 올 때 Flutter 기존의 방식인 BuildContext 객체를 활용 할 정도)
  • MultiProvider 형태로 동시에 여러 Provider 객체를 운용 가능
  • ProxyProvider로 여러 Provider 값을 한데 모아 활용 가능

언제쓸까?

  • 단순 전역적인 상태관리가 필요할 때 사용하기 좋을 것 같다

GetX

Flutter 안에서 쓸 수있는 새로운 프레임워크 같은 라이브러리...
Flutter를 쓰면서 다소 귀찮아질 수 있는 BuildContext와 StatefulWidget의 State객체의 존재를 지우고 많은 것을 GetX로 쓸 수 있다. (Navigator부터 Connection까지...)

Flutter가 어떤 구조로 돌아가는지 생각하게 해주지 않고 GetX 기능을 쓰면 한방에 해결되기에 개발 공부에 필요한 생각을 하지 않게된다는 말도 나온다...

추후에...

profile
개발 지식 공부

0개의 댓글

관련 채용 정보